diff --git a/REWRITE_OVERVIEW.md b/REWRITE_OVERVIEW.md index 91e07767..41bd28cd 100644 --- a/REWRITE_OVERVIEW.md +++ b/REWRITE_OVERVIEW.md @@ -1,87 +1,88 @@ -# Rewrite Overview +# The Pycord Rewrite -Complete overview of every breaking change -made in v3, and why we made them. +v3 aims at making an interface objectively easier to use, faster, and more intuitive than v2. +To do this we have elected that breaking changes in any degree are tolerated. -## Namespace +This means, that with v3 we did not add constraints to differences in UI, or other such. +That means that v3 will feel like a fundamentally different library as compared to v2. +Do not expect v3 to be a drop-in replacement for v2. You will *most likely* have to rewrite +the majority of your bot-facing code. -By far one of the smallest yet most significant -changes is our namespace. -While v2 used to have the discord namespace -for v3 to signify we're not a fork anymore -we changed it to pycord. +We do not have all of these "breaking changes" documented or covered, primarily because they +aren't changes in their own right, they are a new way to do it. V3 is a rewrite of v2, so we +do not have any obligation to hold back on necessary breaking changes, especially since we follow +SemVer. -So instead of `import discord` its just -`import pycord` now. -## Generalized Commands +This document will justify and showcase many of these major breaking changes and show +why they were made. So let's get started. -In v3, commands have been restructured -into one system to allow the easy creation -of new ones. -Instead of having `bot.slash_command` and the other types -like in v2, you instead use our one decorator `bot.command`. +### Bots -Expansibility has been made a priority -for v3, so we also made Commands easy to customize. -Command creators provide a `cls` field to -show which Command class they want to use. -This is required and is not set to any default +Firstly is just your bot. Pycord v3 removes `commands.Bot` (we don't support prefix commands anymore,) +and `Client` for just a single `Bot` class abstracting all of these things. + +We try to make the Bot class as extensible as possible so as to not force you to sub-class, but if needed, +The opportunity is always there. + +`.run` has been removed from Bots, and bots must now have to be started manually. +This forces developers to grapple with async i/o and makes it easier to do things like +database connections before your bot starts. -An example Slash Command could be the following: ```py -import pycord +# decorator for identifying commands, or parent commands. +# sub commands should use a sort of `parent.command` design. -bot = pycord.Bot(...) +@command() +async def echo( + cx: Context, + input: Annotated[str, Option(description="What do you want me to say?")] +): + await cx.say(input) -@bot.command('echo', pycord.ApplicationCommand, description='Command to echo a message') -async def echo(inter: pycord.Interaction, msg: str = pycord.Option(name='message', description='The message to echo')): - await inter.resp.send(msg) -bot.run(...) -``` +# events now use classes instead of strings. +# this is a much more extensible system and makes it easier for +# developers to make extensions -## Extensible Cache +@listen() +async def on_ready(event: Ready): + print(f"I'm online and logged into {event.user.name}!") -Caching has been rebuilt to allow -higher levels of extensibility. -Do you want to cache on redis? Now you can! -It's extremely easy, just subclass our -Store class and rebuild it for your cacher. +async def main(): + bot = Bot( + token='token', + guild_ids=[...], + commands=[await echo.build()] + ) -## Extensions + await bot.start() -### Cogs -Cogs have been completely reformed from the -ground up to build a brand new and better -experience. +if __name__ == "__main__": + asyncio.run(main()) +``` -To show that difference, we have renamed Cogs -to Gears, also because it's particularly a better name. +### Cogs are Gone. -- Basic Cog with Slash Command - ```py - from discord.cogs import Cog - from discord.commands import slash_command +As of version 3, Pycord will no longer be supporting Cogs in their current design. +We will still support grouping your commands and listeners in separate files, just with a +smarter and less object-oriented way. - class MyCog(Cog): - @slash_command - async def dunce(self) -> None: - ... - ``` -- Basic Gear with Slash Command - ```py - import pycord - from pycord.ext.gears import Gear +```py +async def setup(): + return [ + cx, + command, + listener + ] +``` + - gear = Gear(__name__) +# A tiny note - @gear.command('dunce', pycord.ApplicationCommand, type=1, description='duncy command') - async def dunce(inter: pycord.Interaction) -> None: - ... - ``` +There is still more content on the way! The more v3 develops, the more we'll add here :) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 3eb9861a..ca16da35 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,58 +9,58 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'Pycord' +project = "Pycord" -copyright = '2021-present, Pycord Development' -author = 'Pycord Development' -release = '3.0.0' +copyright = "2021-present, Pycord Development" +author = "Pycord Development" +release = "3.0.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('extensions')) +sys.path.insert(0, os.path.abspath("..")) +sys.path.append(os.path.abspath("extensions")) extensions = [ - 'resourcelinks', - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', + "resourcelinks", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", ] -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -autodoc_member_order = 'bysource' -autodoc_typehints = 'none' +autodoc_member_order = "bysource" +autodoc_typehints = "none" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'pydata_sphinx_theme' +html_theme = "pydata_sphinx_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named 'default.css' will overwrite the builtin 'default.css'. -html_static_path = ['_static'] +html_static_path = ["_static"] -html_logo = 'https://raw.githubusercontent.com/Pycord-Development/pycord-v3/main/docs/assets/pycord-v3.png' +html_logo = "https://raw.githubusercontent.com/Pycord-Development/pycord-v3/main/docs/assets/pycord-v3.png" # Any option(s) added to your certain theme. # in this case, Pydata html_theme_options = { - 'footer_items': ['copyright', 'sphinx-version'], + "footer_items": ["copyright", "sphinx-version"], } -html_sidebars = {'**': ['sidebar-nav-bs']} +html_sidebars = {"**": ["sidebar-nav-bs"]} resource_links = { - 'guide': 'https://guide.pycord.dev', - 'repository': 'https://github.com/pycord-development/pycord-v3', + "guide": "https://guide.pycord.dev", + "repository": "https://github.com/pycord-development/pycord-v3", } # Links used for cross-referencing stuff in other documentation intersphinx_mapping = { - 'py': ('https://docs.python.org/3', None), - 'aio': ('https://docs.aiohttp.org/en/stable/', None), + "py": ("https://docs.python.org/3", None), + "aio": ("https://docs.aiohttp.org/en/stable/", None), } diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py index c9383af3..c36fd6f6 100644 --- a/docs/extensions/resourcelinks.py +++ b/docs/extensions/resourcelinks.py @@ -35,10 +35,10 @@ def role( def add_link_role(app: Sphinx) -> None: - app.add_role('resource', make_link_role(app.config.resource_links)) + app.add_role("resource", make_link_role(app.config.resource_links)) def setup(app: Sphinx) -> Dict[str, Any]: - app.add_config_value('resource_links', {}, 'env') - app.connect('builder-inited', add_link_role) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + app.add_config_value("resource_links", {}, "env") + app.connect("builder-inited", add_link_role) + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/examples/.gitkeep b/examples/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/bot.py b/examples/bot.py deleted file mode 100644 index 9946d7d8..00000000 --- a/examples/bot.py +++ /dev/null @@ -1,11 +0,0 @@ -import pycord - -bot = pycord.Bot(intents=pycord.Intents.all()) - - -@bot.listen('on_guild_available') -async def on_ready(guild: pycord.Guild) -> None: - print(f' In Guild: {guild.name}') - - -bot.run('token') diff --git a/examples/cluster.py b/examples/cluster.py deleted file mode 100644 index 84a629b3..00000000 --- a/examples/cluster.py +++ /dev/null @@ -1,11 +0,0 @@ -import pycord - -bot = pycord.Bot(intents=pycord.Intents.all(), shards=6) - - -@bot.listen('on_guild_create') -async def on_ready(guild: pycord.Guild) -> None: - print(f' In Guild: {guild.name}') - - -bot.cluster('token', 2) diff --git a/examples/interactions/commands/application_commands.py b/examples/interactions/commands/application_commands.py deleted file mode 100644 index 6900c8ec..00000000 --- a/examples/interactions/commands/application_commands.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Annotated - -import pycord - -# start the bot with no intents, and with default config -bot = pycord.Bot(intents=pycord.Intents()) - -# the guild id to deploy on. Often used for developing to -# avoid having to wait the extraneous amount of time Discord has for global -# commands -GUILD_ID: int | pycord.MissingEnum = pycord.MISSING - - -# make a chat input command which -# is named favorite and that displays -# an autocompleted list of animes to pick from -@bot.command(guild_id=GUILD_ID) -# make a function for what to do once the user -# has completed their input. -# this has the option anime, displayed as a Parameter, -# which is parsed by Pycord to give you the information the user gave. -async def favorite( - ctx: pycord.Context, - # The name of this option, - # can be set to anything but - # try to keep it short - anime: Annotated[ - str, - pycord.Option( - # The description for this option, - # this is a longer version of name displaying - # more detail and technicalities - description='Your favorite Anime Show', - # this just sets it so the user cannot proceed without - # entering this option - required=True, - # enables autocomplete on Discord's side - autocomplete=True, - # these are the choices the user can pick. - # the first value is the name, which is what - # the user will see. The second is the value, which is what - # you, or the bot, will see. - choices=[ - pycord.CommandChoice('Attack on Titan'), - pycord.CommandChoice("JoJo's Bizzare Adventure"), - pycord.CommandChoice('Cowboy Bebop'), - pycord.CommandChoice('Hunter x Hunter'), - pycord.CommandChoice('Spy x Family'), - ], - ), - ], -): - """Pick which one is your favorite anime""" - - # checks the value of the int - # and if it matches up to an anime, - # it responds with a custom response. - match anime: - case 'Attack on Titan': - await ctx.send('It seems like you like Attack on Titan, Nice!') - case "JoJo's Bizzare Adventure": - await ctx.send("おにいちゃんありがとう. You like JoJo's Bizzare Adventure. Nice!") - case 'Cowboy Bebop': - await ctx.send('良い!あなたはカウボーイビバップが好きです') - case 'Hunter x Hunter': - await ctx.send( - 'I ran out of responses... Well anyway, you like Hunter x Hunter which is Nice!' - ) - case 'Spy x Family': - await ctx.send( - 'I have a friend which really likes this anime, ' - "it's good seeing you like it too. Of course, Spy x Family!" - ) - - -# run the bot with the token. -# PLEASE REMEMBER TO CHANGE! -bot.run('token') diff --git a/examples/interactions/components/button.py b/examples/interactions/components/button.py deleted file mode 100644 index b57bc207..00000000 --- a/examples/interactions/components/button.py +++ /dev/null @@ -1,37 +0,0 @@ -import pycord - -# initiate a bot with 0 intents -bot = pycord.Bot(intents=pycord.Intents()) - -# create a house to store our components -house = pycord.House() - -# add a button to our house -# as well as add a callback for when its interacted with -@house.button(pycord.ButtonStyle.GREEN, 'yes') -async def yes_button(inter: pycord.Interaction) -> None: - await inter.resp.send('you said yes!') - - -# add a "no" button in direct reply to -# our "yes" button -# responds the same as yes but with a touch of no -@house.button(pycord.ButtonStyle.RED, 'no') -async def no_button(inter: pycord.Interaction) -> None: - await inter.resp.send('you said no :(') - - -# listen for the on_message event -@bot.listen('on_message') -async def on_message(message: pycord.Message) -> None: - # check if the content in this message aligns - # with the wanted content of our command - if message.content == '!!yesorno': - # send the house singularly - # if you want to send multiple, just do houses= and set the value as a list - await message.channel.send('Heres your house mate', house=house) - - -# run the bot with the token. -# PLEASE REMEMBER TO CHANGE! -bot.run('token') diff --git a/examples/interactions/components/text_input.py b/examples/interactions/components/text_input.py deleted file mode 100644 index 8fdd9749..00000000 --- a/examples/interactions/components/text_input.py +++ /dev/null @@ -1,55 +0,0 @@ -import pycord -from pycord.ui import text_input - -# initiate a bot with 0 intents -bot = pycord.Bot(intents=pycord.Intents()) - -# make a modal which holds our text inputs -favorite_friend_modal = text_input.Modal("Who's your favorite friend?") -# add a text input into this modal -favorite_friend_modal.add_text_input( - # instantiate a TextInput class - text_input.TextInput( - # the name of the text input - # in this case "Friend Name" - 'Friend Name', - # should this be styled as a paragraph - # or shortened? In this case short since it's only names - pycord.TextInputStyle.SHORT, - # makes this TextInput required to proceed - required=True, - # the placeholder (or example) value set - placeholder='Michael.. Bob.. Emre..', - ) -) - - -# a function which runs every time -# a favorite_friend_modal is finished -@favorite_friend_modal.on_call -async def on_favorite_friend_call(inter: pycord.Interaction, name: str) -> None: - # sends the friends name in chat - await inter.resp.send(f"{inter.user.name}'s favorite friend is {name}!") - - -# create an app command to send the modal in -@bot.command( - # names the app command "favorite_friend" - 'favorite_friend', - # sets the command type to Application - pycord.ApplicationCommand, - # specifies this should be a chat input (slash) command - type=pycord.ApplicationCommandType.CHAT_INPUT, - # the command description - description='Describe your favorite friend within a modal', - # a test guild to append this command to - guild_id=None, -) -async def favorite_friend(inter: pycord.Interaction) -> None: - # send the modal to the user - await inter.resp.send_modal(modal=favorite_friend_modal) - - -# run the bot with the token -# PLEASE REMEMBER TO CHANGE! -bot.run('token') diff --git a/examples/superspeed_rate_limiting.py b/examples/superspeed_rate_limiting.py deleted file mode 100644 index 7227df49..00000000 --- a/examples/superspeed_rate_limiting.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import logging -from typing import Any - -import pycord - -logging.basicConfig(level=logging.DEBUG) -api = pycord.HTTPClient('token') -GUILD_ID = 0 - - -async def spam_channels() -> None: - channels: list[dict[str, Any]] = [] - - tasks: list[asyncio.Task] = [ - api.request( - 'POST', - pycord.Route('/guilds/{guild_id}/channels', guild_id=GUILD_ID), - {'name': 'rate-limit-test'}, - ) - for _ in range(50) - ] - - channels = await asyncio.gather(*tasks) - tasks.clear() - - tasks.extend( - api.request( - 'DELETE', pycord.Route('/channels/{channel_id}', channel_id=channel['id']) - ) - for channel in channels - ) - - await asyncio.gather(*tasks) - await api.close_session() - - -asyncio.run(spam_channels()) diff --git a/pycord/__init__.py b/pycord/__init__.py index 74d500d3..82d70acf 100644 --- a/pycord/__init__.py +++ b/pycord/__init__.py @@ -1,43 +1,15 @@ """ Pycord ~~~~~~ -Making Bots Happen. +A library for modern Discord bots. -:copyright: 2021-present Pycord Development +:copyright: 2021-present Pycord :license: MIT """ + from ._about import * -from .api import * -from .application import * -from .audit_log import * -from .auto_moderation import * from .bot import * -from .channel import * from .color import * -from .commands import * -from .connection import * -from .embed import * from .enums import * -from .events import * from .flags import * -from .gateway import * -from .guild import * -from .guild_template import * -from .http import * -from .integration import * -from .interaction import * -from .interface import * -from .invite import * -from .media import * -from .member import * -from .message import * -from .role import * -from .snowflake import * -from .stage_instance import * -from .state import * -from .team import * -from .user import * from .utils import * -from .voice import * -from .webhook import * -from .welcome_screen import * diff --git a/pycord/__main__.py b/pycord/__main__.py deleted file mode 100644 index 48459570..00000000 --- a/pycord/__main__.py +++ /dev/null @@ -1,36 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -"""Cli processes.""" -import platform -import sys - -import pycord - - -def main() -> None: - version = pycord.__version__ - python_version = platform.python_version() - sys.stderr.write(f'Running on Pycord Version {version},') - sys.stderr.write(f' with Python version {python_version}.') - - -if __name__ == '__main__': - main() diff --git a/pycord/_about.py b/pycord/_about.py index 60d66606..22d1e7d5 100644 --- a/pycord/_about.py +++ b/pycord/_about.py @@ -22,34 +22,34 @@ import logging import typing -__title__: str = 'pycord' -__author__: str = 'Pycord Development' -__license__: str = 'MIT' -__copyright__: str = 'Copyright 2021-present Pycord Development' -__version__: str = '3.0.0' -__git_sha1__: str = 'HEAD' +__title__: str = "pycord" +__author__: str = "Pycord Development" +__license__: str = "MIT" +__copyright__: str = "Copyright 2021-present Pycord Development" +__version__: str = "3.0.0" +__git_sha1__: str = "HEAD" class VersionInfo(typing.NamedTuple): major: int minor: int micro: int - releaselevel: typing.Literal['alpha', 'beta', 'candidate', 'final'] + releaselevel: typing.Literal["alpha", "beta", "candidate", "final"] serial: int version_info: VersionInfo = VersionInfo( - major=3, minor=0, micro=0, releaselevel='alpha', serial=0 + major=3, minor=0, micro=0, releaselevel="alpha", serial=0 ) logging.getLogger(__name__).addHandler(logging.NullHandler()) __all__: typing.Sequence[str] = ( - '__title__', - '__author__', - '__license__', - '__copyright__', - '__version__', - 'VersionInfo', - 'version_info', + "__title__", + "__author__", + "__license__", + "__copyright__", + "__version__", + "VersionInfo", + "version_info", ) diff --git a/pycord/api/__init__.py b/pycord/api/__init__.py deleted file mode 100644 index a79636a8..00000000 --- a/pycord/api/__init__.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -pycord.api -~~~~~~~~~~ -Implementation of the Discord API. - -:copyright: 2021-present Pycord Development -:license: MIT -""" -import logging -import sys -from typing import Any, Sequence - -from aiohttp import BasicAuth, ClientSession, FormData, __version__ as aiohttp_version - -from pycord._about import __version__ - -from .. import utils -from ..errors import BotException, Forbidden, HTTPException, InternalError, NotFound -from ..file import File -from ..utils import dumps -from .execution import Executer -from .route import BaseRoute, Route -from .routers import * -from .routers.scheduled_events import ScheduledEvents -from .routers.user import Users - -__all__: Sequence[str] = ('Route', 'BaseRoute', 'HTTPClient') - -_log = logging.getLogger(__name__) - - -class HTTPClient( - ApplicationCommands, - ApplicationRoleConnections, - AuditLogs, - AutoModeration, - Channels, - Emojis, - Guilds, - Messages, - ScheduledEvents, - Users, -): - def __init__( - self, - token: str | None = None, - base_url: str = 'https://discord.com/api/v10', - proxy: str | None = None, - proxy_auth: BasicAuth | None = None, - verbose: bool = False, - ) -> None: - self.base_url = base_url - self._proxy = proxy - self._proxy_auth = proxy_auth - self._headers = { - 'User-Agent': 'DiscordBot (https://pycord.dev, {0}) Python/{1[0]}.{1[1]} aiohttp/{2}'.format( - __version__, sys.version_info, aiohttp_version - ), - } - if token: - self._headers['Authorization'] = f'Bot {token}' - self.verbose = verbose - - self._session: None | ClientSession = None - self._executers: list[Executer] = [] - - async def create_session(self) -> None: - self._session = ClientSession() - - async def close_session(self) -> None: - await self._session.close() - self._session = None - - async def request( - self, - method: str, - route: BaseRoute, - data: dict[str, Any] | None = None, - files: list[File] | None = None, - form: list[dict[str, Any]] | None = None, - *, - reason: str | None = None, - query_params: dict[str, str] | None = None, - ) -> REQUEST_RETURN: - endpoint = route.merge(self.base_url) - - if self._session is None: - await self.create_session() - - headers = self._headers.copy() - - if reason: - headers['X-Audit-Log-Reason'] = reason - - if data: - data: str = dumps(data=data) - headers.update({'Content-Type': 'application/json'}) - - if form and data: - form.append({'name': 'payload_json', 'value': data}) - - if files: - if not form: - form = [] - - for idx, file in enumerate(files): - form.append( - { - 'name': f'files[{idx}]', - 'value': file.file.read(), - 'filename': file.filename, - 'content_type': 'application/octet-stream', - } - ) - - _log.debug(f'Requesting to {endpoint} with {data}, {headers}') - - for executer in self._executers: - if executer.is_global or executer.route == route: - _log.debug(f'Pausing request to {endpoint}: Found rate limit executer') - await executer.wait() - - for try_ in range(5): - if files: - for file in files: - file.reset(try_) - - if form: - data = FormData(quote_fields=False) - for params in form: - data.add_field(**params) - - r = await self._session.request( - method, - endpoint, - data=data, - headers=headers, - proxy=self._proxy, - proxy_auth=self._proxy_auth, - params=query_params, - ) - _log.debug(f'Received back {await r.text()}') - - data = await utils._text_or_json(cr=r) - - if r.status == 429: - _log.debug(f'Request to {endpoint} failed: Request returned rate limit') - executer = Executer(route=route) - - self._executers.append(executer) - await executer.executed( - reset_after=data['retry_after'], - is_global=r.headers.get('X-RateLimit-Scope') == 'global', - limit=int(r.headers.get('X-RateLimit-Limit', 10)), - ) - self._executers.remove(executer) - continue - - elif r.status == 403: - raise Forbidden(resp=r, data=data) - elif r.status == 404: - raise NotFound(resp=r, data=data) - elif r.status == 500: - raise InternalError(resp=r, data=data) - elif r.ok: - return data - else: - if self.verbose: - raise BotException(r, data) - else: - raise HTTPException(resp=r, data=data) - - async def get_gateway_bot(self) -> dict[str, Any]: - return await self.request('GET', Route('/gateway/bot')) diff --git a/pycord/api/execution/__init__.py b/pycord/api/execution/__init__.py deleted file mode 100644 index 07e4bed8..00000000 --- a/pycord/api/execution/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -pycord.execution -~~~~~~~~~~~~~~~~ -Rate Limit execution for the Discord API - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .executer import * diff --git a/pycord/api/execution/executer.py b/pycord/api/execution/executer.py deleted file mode 100644 index 33a3f3b8..00000000 --- a/pycord/api/execution/executer.py +++ /dev/null @@ -1,71 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -import asyncio - -from ..route import BaseRoute - - -class Executer: - def __init__(self, route: BaseRoute) -> None: - self.route = route - self.is_global: bool | None = None - self._request_queue: asyncio.Queue[asyncio.Event] | None = None - self.rate_limited: bool = False - - async def executed( - self, reset_after: int | float, limit: int, is_global: bool - ) -> None: - self.rate_limited = True - self.is_global = is_global - self._reset_after = reset_after - self._request_queue = asyncio.Queue() - - await asyncio.sleep(reset_after) - - self.is_global = False - - # NOTE: This could break if someone did a second global rate limit somehow - requests_passed: int = 0 - for _ in range(self._request_queue.qsize() - 1): - if requests_passed == limit: - requests_passed = 0 - if not is_global: - await asyncio.sleep(reset_after) - else: - await asyncio.sleep(5) - - requests_passed += 1 - e = await self._request_queue.get() - e.set() - - async def wait(self) -> None: - if not self.rate_limited: - return - - event = asyncio.Event() - - if self._request_queue: - self._request_queue.put_nowait(event) - else: - raise ValueError( - 'Request queue does not exist, rate limit may have been solved.' - ) - await event.wait() diff --git a/pycord/api/route.py b/pycord/api/route.py deleted file mode 100644 index 9e1b53c4..00000000 --- a/pycord/api/route.py +++ /dev/null @@ -1,84 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Sequence - -from pycord.types import Snowflake - -__all__: Sequence[str] = ('Route', 'BaseRoute') - - -class BaseRoute: - guild_id: int | None - channel_id: int | None - webhook_id: int | None - webhook_token: str | None - - def __init__( - self, - path: str, - guild_id: Snowflake | None = None, - channel_id: Snowflake | None = None, - webhook_id: Snowflake | None = None, - webhook_token: str | None = None, - **parameters: str | int, - ) -> None: - ... - - def merge(self, url: str) -> str: - pass - - -class Route(BaseRoute): - def __init__( - self, - path: str, - guild_id: Snowflake | None = None, - channel_id: Snowflake | None = None, - webhook_id: Snowflake | None = None, - webhook_token: str | None = None, - **parameters: str | int, - ): - self.path = path - - # major parameters - self.guild_id = guild_id - self.channel_id = channel_id - self.webhook_id = webhook_id - self.webhook_token = webhook_token - - self.parameters = parameters - - def merge(self, url: str): - return url + self.path.format( - guild_id=self.guild_id, - channel_id=self.channel_id, - webhook_id=self.webhook_id, - webhook_token=self.webhook_token, - **self.parameters, - ) - - def __eq__(self, route: 'Route') -> bool: - return ( - route.channel_id == self.channel_id - or route.guild_id == self.guild_id - or route.webhook_id == self.webhook_id - or route.webhook_token == self.webhook_token - ) diff --git a/pycord/api/routers/__init__.py b/pycord/api/routers/__init__.py deleted file mode 100644 index 21040605..00000000 --- a/pycord/api/routers/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -pycord.api.routers -~~~~~~~~~~~~~~~~~~ -Implementation of Discord API Routes - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .application_commands import * -from .application_role_connection_metadata import * -from .audit_logs import * -from .auto_moderation import * -from .base import * -from .channels import * -from .emojis import * -from .guilds import * -from .messages import * diff --git a/pycord/api/routers/application_commands.py b/pycord/api/routers/application_commands.py deleted file mode 100644 index bdba51fc..00000000 --- a/pycord/api/routers/application_commands.py +++ /dev/null @@ -1,333 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import ATYPE, ApplicationCommandOption -from ...types.application_commands import ( - ApplicationCommand, - ApplicationCommandPermissions, - GuildApplicationCommandPermissions, -) -from ...types.interaction import InteractionResponse -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class ApplicationCommands(BaseRouter): - async def get_global_application_commands( - self, application_id: Snowflake, with_localizations: bool = False - ): - return await self.request( - 'GET', - Route( - '/applications/{application_id}/commands', application_id=application_id - ), - query_params={'with_localizations': str(with_localizations).lower()}, - ) - - async def create_global_application_command( - self, - application_id: Snowflake, - name: str, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - options: list[ApplicationCommandOption] | MissingEnum = MISSING, - default_member_permissions: str | None | MissingEnum = MISSING, - dm_permission: MissingEnum | bool | None = MISSING, - default_permission: bool | MissingEnum = MISSING, - type: ATYPE | MissingEnum = MISSING, - ): - data = remove_undefined( - name=name, - name_localizations=name_localizations, - description=description, - description_localizations=description_localizations, - options=options, - default_member_permissions=default_member_permissions, - dm_permission=dm_permission, - default_permission=default_permission, - type=type, - ) - - return await self.request( - 'POST', - Route( - '/applications/{application_id}/commands', application_id=application_id - ), - data=data, - ) - - async def edit_global_application_command( - self, - application_id: Snowflake, - command_id: Snowflake, - name: str | MissingEnum = MISSING, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - options: list[ApplicationCommandOption] | MissingEnum = MISSING, - default_member_permissions: str | None | MissingEnum = MISSING, - dm_permission: MissingEnum | bool | None = MISSING, - default_permission: bool | MissingEnum = MISSING, - type: ATYPE | MissingEnum = MISSING, - ): - data = remove_undefined( - name=name, - name_localizations=name_localizations, - description=description, - description_localizations=description_localizations, - options=options, - default_member_permissions=default_member_permissions, - dm_permission=dm_permission, - default_permission=default_permission, - type=type, - ) - - return await self.request( - 'PATCH', - Route( - '/applications/{application_id}/commands/{command_id}', - application_id=application_id, - command_id=command_id, - ), - data=data, - ) - - async def delete_global_application_command( - self, - application_id: Snowflake, - command_id: Snowflake, - ): - return await self.request( - 'DELETE', - Route( - '/applications/{application_id}/commands/{command_id}', - application_id=application_id, - command_id=command_id, - ), - ) - - async def get_guild_application_commands( - self, - application_id: Snowflake, - guild_id: Snowflake, - with_localizations: bool = False, - ): - return await self.request( - 'GET', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands', - application_id=application_id, - guild_id=guild_id, - ), - query_params={'with_localizations': str(with_localizations).lower()}, - ) - - async def create_guild_application_command( - self, - application_id: Snowflake, - guild_id: Snowflake, - name: str, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - options: list[ApplicationCommandOption] | MissingEnum = MISSING, - default_member_permissions: str | None | MissingEnum = MISSING, - dm_permission: MissingEnum | bool | None = MISSING, - default_permission: bool | MissingEnum = MISSING, - type: ATYPE | MissingEnum = MISSING, - ): - data = remove_undefined( - name=name, - name_localizations=name_localizations, - description=description, - description_localizations=description_localizations, - options=options, - default_member_permissions=default_member_permissions, - dm_permission=dm_permission, - default_permission=default_permission, - type=type, - ) - - return await self.request( - 'POST', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands', - application_id=application_id, - guild_id=guild_id, - ), - data=data, - ) - - async def edit_guild_application_command( - self, - application_id: Snowflake, - command_id: Snowflake, - guild_id: Snowflake, - name: str | MissingEnum = MISSING, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - options: list[ApplicationCommandOption] | MissingEnum = MISSING, - default_member_permissions: str | None | MissingEnum = MISSING, - dm_permission: MissingEnum | bool | None = MISSING, - default_permission: bool | MissingEnum = MISSING, - type: ATYPE | MissingEnum = MISSING, - ): - data = remove_undefined( - name=name, - name_localizations=name_localizations, - description=description, - description_localizations=description_localizations, - options=options, - default_member_permissions=default_member_permissions, - dm_permission=dm_permission, - default_permission=default_permission, - type=type, - ) - - return await self.request( - 'PATCH', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}', - application_id=application_id, - guild_id=guild_id, - command_id=command_id, - ), - data=data, - ) - - async def delete_guild_application_command( - self, - application_id: Snowflake, - guild_id: Snowflake, - command_id: Snowflake, - ): - return await self.request( - 'DELETE', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}', - application_id=application_id, - command_id=command_id, - guild_id=guild_id, - ), - ) - - async def bulk_overwrite_global_commands( - self, application_id: Snowflake, application_commands: list[ApplicationCommand] - ) -> list[ApplicationCommand]: - await self.request( - 'PUT', - Route( - '/applications/{application_id}/commands', application_id=application_id - ), - application_commands, - ) - - async def bulk_overwrite_guild_commands( - self, - application_id: Snowflake, - guild_id: Snowflake, - application_commands: list[ApplicationCommand], - ) -> list[ApplicationCommand]: - await self.request( - 'PUT', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands', - application_id=application_id, - guild_id=guild_id, - ), - application_commands, - ) - - async def get_guild_application_command_permissions( - self, application_id: Snowflake, guild_id: Snowflake - ): - return await self.request( - 'GET', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands/permissions', - guild_id=guild_id, - application_id=application_id, - ), - ) - - async def get_application_command_permissions( - self, application_id: Snowflake, guild_id: Snowflake, command_id: Snowflake - ): - return await self.request( - 'GET', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions', - guild_id=guild_id, - application_id=application_id, - command_id=command_id, - ), - ) - - async def edit_application_command_permissions( - self, - application_id: Snowflake, - guild_id: Snowflake, - command_id: Snowflake, - permissions: list[ApplicationCommandPermissions], - ) -> GuildApplicationCommandPermissions: - return await self.request( - 'PUT', - Route( - '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions', - application_id=application_id, - guild_id=guild_id, - command_id=command_id, - ), - {'permissions': permissions}, - ) - - # interactions - async def create_interaction_response( - self, - interaction_id: Snowflake, - interaction_token: str, - response: InteractionResponse, - ) -> None: - await self.request( - 'POST', - Route( - '/interactions/{interaction_id}/{interaction_token}/callback', - interaction_id=interaction_id, - interaction_token=interaction_token, - ), - data=response, - ) - - async def get_original_interaction_response( - self, interaction_id: Snowflake, interaction_token: str - ) -> InteractionResponse: - await self.request( - 'GET', - Route( - '/interactions/{interaction_id}/{interaction_token}/messages/@original', - interaction_id=interaction_id, - interaction_token=interaction_token, - ), - ) diff --git a/pycord/api/routers/application_role_connection_metadata.py b/pycord/api/routers/application_role_connection_metadata.py deleted file mode 100644 index 0180851c..00000000 --- a/pycord/api/routers/application_role_connection_metadata.py +++ /dev/null @@ -1,52 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...snowflake import Snowflake -from ...types import ApplicationRoleConnectionMetadata -from ..route import Route -from .base import BaseRouter - - -class ApplicationRoleConnections(BaseRouter): - async def get_application_role_connection_metadata_records( - self, - application_id: Snowflake, - ) -> list[ApplicationRoleConnectionMetadata]: - return await self.request( - 'GET', - Route( - '/applications/{application_id}/role-connections/metadata', - application_id=application_id, - ), - ) - - async def update_application_role_connection_metadata_records( - self, - application_id: Snowflake, - records: list[ApplicationRoleConnectionMetadata], - ) -> list[ApplicationRoleConnectionMetadata]: - return self.request( - 'PUT', - Route( - '/applications/{application_id}/role-connections/metadata', - application_id=application_id, - ), - records, - ) diff --git a/pycord/api/routers/audit_logs.py b/pycord/api/routers/audit_logs.py deleted file mode 100644 index 6ddea4d6..00000000 --- a/pycord/api/routers/audit_logs.py +++ /dev/null @@ -1,54 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import AUDIT_LOG_EVENT_TYPE, AuditLog -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class AuditLogs(BaseRouter): - async def get_guild_audit_log( - self, - guild_id: Snowflake, - *, - user_id: Snowflake | MissingEnum = MISSING, - action_type: AUDIT_LOG_EVENT_TYPE | MissingEnum = MISSING, - before: Snowflake | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> AuditLog: - params = { - 'user_id': user_id, - 'action_type': action_type, - 'before': before, - 'after': after, - 'limit': limit, - } - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/audit-logs', - guild_id=guild_id, - ), - query_params=remove_undefined(params), - ) diff --git a/pycord/api/routers/auto_moderation.py b/pycord/api/routers/auto_moderation.py deleted file mode 100644 index 8bdebd5e..00000000 --- a/pycord/api/routers/auto_moderation.py +++ /dev/null @@ -1,140 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import ( - AUTO_MODERATION_EVENT_TYPES, - AUTO_MODERATION_TRIGGER_TYPES, - AutoModerationAction, - AutoModerationRule, - AutoModerationTriggerMetadata, -) -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class AutoModeration(BaseRouter): - async def list_auto_moderation_rules_for_guild( - self, guild_id: Snowflake - ) -> list[AutoModerationRule]: - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/auto-moderation/rules', - guild_id=guild_id, - ), - ) - - async def get_auto_moderation_rule( - self, - guild_id: Snowflake, - rule_id: Snowflake, - ) -> AutoModerationRule: - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', - guild_id=guild_id, - rule_id=rule_id, - ), - ) - - async def create_auto_moderation_rule( - self, - guild_id: Snowflake, - *, - name: str, - event_type: AUTO_MODERATION_EVENT_TYPES, - trigger_type: AUTO_MODERATION_TRIGGER_TYPES, - actions: list[AutoModerationAction], - trigger_metadata: AutoModerationTriggerMetadata | MissingEnum = MISSING, - enabled: bool = False, - exempt_roles: list[Snowflake] | MissingEnum = MISSING, - exempt_channels: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> AutoModerationRule: - data = { - 'name': name, - 'event_type': event_type, - 'trigger_type': trigger_type, - 'trigger_metadata': trigger_metadata, - 'actions': actions, - 'enabled': enabled, - 'exampt_roles': exempt_roles, - 'exempt_channels': exempt_channels, - } - return await self.request( - 'POST', - Route( - '/guilds/{guild_id}/auto-moderation/rules', - guild_id=guild_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def modify_auto_moderation_rule( - self, - guild_id: Snowflake, - rule_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - event_type: AUTO_MODERATION_EVENT_TYPES | MissingEnum = MISSING, - actions: list[AutoModerationAction] | MissingEnum = MISSING, - trigger_metadata: AutoModerationTriggerMetadata | MissingEnum | None = MISSING, - enabled: bool | MissingEnum = MISSING, - exempt_roles: list[Snowflake] | MissingEnum = MISSING, - exempt_channels: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> AutoModerationRule: - data = { - 'name': name, - 'event_type': event_type, - 'trigger_metadata': trigger_metadata, - 'actions': actions, - 'enabled': enabled, - 'exampt_roles': exempt_roles, - 'exempt_channels': exempt_channels, - } - return await self.request( - 'PATCH', - Route( - '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', - guild_id=guild_id, - rule_id=rule_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def delete_auto_moderation_rule( - self, guild_id: Snowflake, rule_id: Snowflake, *, reason: str | None = None - ) -> None: - return await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', - guild_id=guild_id, - rule_id=rule_id, - ), - reason=reason, - ) diff --git a/pycord/api/routers/base.py b/pycord/api/routers/base.py deleted file mode 100644 index 3a657e66..00000000 --- a/pycord/api/routers/base.py +++ /dev/null @@ -1,41 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Any - -from ...file import File -from ..route import BaseRoute - -REQUEST_RETURN = str | dict[str, Any] | list[Any] | bytes - - -class BaseRouter: - async def request( - self, - method: str, - route: BaseRoute, - data: dict[str, Any] | None = None, - files: list[File] | None = None, - form: list[dict[str, Any]] | None = None, - *, - reason: str | None = None, - query_params: dict[str, str] | None = None, - ) -> REQUEST_RETURN: - ... diff --git a/pycord/api/routers/channels.py b/pycord/api/routers/channels.py deleted file mode 100644 index 224fce9f..00000000 --- a/pycord/api/routers/channels.py +++ /dev/null @@ -1,513 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import ( - CTYPE, - Channel, - DefaultReaction, - FollowedChannel, - ForumTag, - ForumThreadMessageParams, - Invite, - ListThreadsResponse, - Message, - Overwrite, - ThreadMember, -) -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class Channels(BaseRouter): - async def get_channel( - self, - channel_id: Snowflake, - ) -> Channel: - return await self.request( - 'GET', Route('/channels/{channel_id}', channel_id=channel_id) - ) - - async def modify_channel( - self, - channel_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - # Group DM Only - icon: bytes | MissingEnum = MISSING, # TODO - # Thread Only - archived: bool | MissingEnum = MISSING, - auto_archive_duration: int | MissingEnum = MISSING, - locked: bool | MissingEnum = MISSING, - invitable: bool | MissingEnum = MISSING, - applied_tags: list[Snowflake] | MissingEnum = MISSING, - # Thread & Guild Channels - rate_limit_per_user: int | None | MissingEnum = MISSING, - flags: int | None | MissingEnum = MISSING, - # Guild Channels Only - type: CTYPE | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - topic: str | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - bitrate: int | None | MissingEnum = MISSING, - user_imit: int | None | MissingEnum = MISSING, - permission_overwrites: list[Overwrite] | None | MissingEnum = MISSING, - parent_id: int | None | MissingEnum = MISSING, - rtc_region: str | None | MissingEnum = MISSING, - video_quality_mode: int | None | MissingEnum = MISSING, - default_auto_archive_duration: int | None | MissingEnum = MISSING, - available_tags: list[ForumTag] | None | MissingEnum = MISSING, - default_reaction_emoji: DefaultReaction | None | MissingEnum = MISSING, - default_thread_rate_limit_per_user: int | MissingEnum = MISSING, - default_sort_order: int | None | MissingEnum = MISSING, - default_forum_layout: int | MissingEnum = MISSING, - # Reason - reason: str | None = None, - ) -> Channel: - data = { - 'name': name, - 'archived': archived, - 'auto_archive_duration': auto_archive_duration, - 'locked': locked, - 'invitable': invitable, - 'applied_tags': applied_tags, - 'rate_limit_per_user': rate_limit_per_user, - 'flags': flags, - 'type': type, - 'position': position, - 'topic': topic, - 'nsfw': nsfw, - 'bitrate': bitrate, - 'user_limit': user_imit, - 'permission_overwrites': permission_overwrites, - 'parent_id': parent_id, - 'rtc_region': rtc_region, - 'video_quality_mode': video_quality_mode, - 'default_auto_archive_duration': default_auto_archive_duration, - 'available_tags': available_tags, - 'default_reaction_emoji': default_reaction_emoji, - 'default_thread_rate_limit_per_user': default_thread_rate_limit_per_user, - 'default_sort_order': default_sort_order, - 'default_forum_layout': default_forum_layout, - } - return await self.request( - 'PATCH', - Route('/channels/{channel_id}', channel_id=channel_id), - remove_undefined(**data), - reason=reason, - ) - - async def delete_channel( - self, - channel_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route('/channels/{channel_id}', channel_id=channel_id), - reason=reason, - ) - - async def edit_channel_permissions( - self, - channel_id: Snowflake, - overwrite_id: Snowflake, - *, - type: int, - allow: int | None | MissingEnum = MISSING, - deny: int | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> None: - data = { - 'allow': str(allow), - 'deny': str(deny), - 'type': type, - } - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/permissions/{overwrite_id}', - channel_id=channel_id, - overwrite_id=overwrite_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def get_channel_invites( - self, - channel_id: Snowflake, - ) -> list[Invite]: - return await self.request( - 'GET', Route('/channels/{channel_id}/invites', channel_id=channel_id) - ) - - async def create_channel_invite( - self, - channel_id: Snowflake, - *, - max_age: int | MissingEnum = MISSING, - max_uses: int | MissingEnum = MISSING, - temporary: bool | MissingEnum = MISSING, - unique: bool | MissingEnum = MISSING, - target_type: int | MissingEnum = MISSING, - target_user_id: Snowflake | MissingEnum = MISSING, - target_application_id: Snowflake | MissingEnum = MISSING, - reason: str | None = None, - ) -> Invite: - data = { - 'max_age': max_age, - 'max_uses': max_uses, - 'temporary': temporary, - 'unique': unique, - 'target_type': target_type, - 'target_user_id': target_user_id, - 'target_application_id': target_application_id, - } - return await self.request( - 'POST', - Route('/channels/{channel_id}/invites', channel_id=channel_id), - remove_undefined(**data), - reason=reason, - ) - - async def delete_channel_permission( - self, - channel_id: Snowflake, - overwrite_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/permissions/{overwrite_id}', - channel_id=channel_id, - overwrite_id=overwrite_id, - ), - reason=reason, - ) - - async def follow_announcement_channel( - self, - channel_id: Snowflake, - *, - webhook_channel_id: Snowflake, - ) -> FollowedChannel: - data = { - 'webhook_channel_id': webhook_channel_id, - } - return await self.request( - 'POST', - Route( - '/channels/{channel_id}/followers', - channel_id=channel_id, - ), - data, - ) - - async def trigger_typing_indicator( - self, - channel_id: Snowflake, - ) -> None: - await self.request( - 'POST', Route('/channels/{channel_id}/typing', channel_id=channel_id) - ) - - async def get_pinned_messages( - self, - channel_id: Snowflake, - ) -> list[Message]: - return await self.request( - 'GET', Route('/channels/{channel_id}/pins', channel_id=channel_id) - ) - - async def group_dm_add_recipient( - self, - channel_id: Snowflake, - user_id: Snowflake, - *, - access_token: str, - nick: str | None | MissingEnum = MISSING, - ) -> None: - data = { - 'access_token': access_token, - 'nick': nick, - } - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/recipients/{user_id}', - channel_id=channel_id, - user_id=user_id, - ), - data, - ) - - async def group_dm_remove_recipient( - self, - channel_id: Snowflake, - user_id: Snowflake, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/recipients/{user_id}', - channel_id=channel_id, - user_id=user_id, - ), - ) - - async def start_thread_from_message( - self, - channel_id: Snowflake, - message_id: Snowflake, - *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> Channel: - data = { - 'name': name, - 'auto_archive_duration': auto_archive_duration, - 'rate_limit_per_user': rate_limit_per_user, - } - return await self.request( - 'POST', - Route( - '/channels/{channel_id}/messages/{message_id}/threads', - channel_id=channel_id, - message_id=message_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def start_thread_without_message( - self, - channel_id: Snowflake, - *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - type: CTYPE | MissingEnum = MISSING, - invitable: bool | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> Channel: - data = { - 'name': name, - 'auto_archive_duration': auto_archive_duration, - 'type': type, - 'invitable': invitable, - 'rate_limit_per_user': rate_limit_per_user, - } - return await self.request( - 'POST', - Route( - '/channels/{channel_id}/threads', - channel_id=channel_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def start_thread_in_forum_channel( - self, - channel_id: Snowflake, - *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - message: ForumThreadMessageParams, - applied_tags: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> Channel: - data = { - 'name': name, - 'auto_archive_duration': auto_archive_duration, - 'rate_limit_per_user': rate_limit_per_user, - } - return await self.request( - 'POST', - Route( - '/channels/{channel_id}/threads', - channel_id=channel_id, - ), - remove_undefined(**data), - reason=reason, - ) - - async def join_thread( - self, - channel_id: Snowflake, - ) -> None: - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/thread-members/@me', - channel_id=channel_id, - ), - ) - - async def add_thread_member( - self, - channel_id: Snowflake, - user_id: Snowflake, - ) -> None: - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/thread-members/{user_id}', - channel_id=channel_id, - user_id=user_id, - ), - ) - - async def leave_thread( - self, - channel_id: Snowflake, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/thread-members/@me', - channel_id=channel_id, - ), - ) - - async def remove_thread_member( - self, - channel_id: Snowflake, - user_id: Snowflake, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/thread-members/{user_id}', - channel_id=channel_id, - user_id=user_id, - ), - ) - - async def get_thread_member( - self, - channel_id: Snowflake, - user_id: Snowflake, - *, - with_member: bool | MissingEnum = MISSING, - ) -> ThreadMember: - params = { - 'with_member': with_member, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/thread-members/{user_id}', - channel_id=channel_id, - user_id=user_id, - ), - params=remove_undefined(params), - ) - - async def list_thread_members( - self, - channel_id: Snowflake, - *, - with_member: bool | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> list[ThreadMember]: - params = { - 'with_member': with_member, - 'after': after, - 'limit': limit, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/thread-members', - channel_id=channel_id, - ), - params=remove_undefined(params), - ) - - async def list_public_archived_threads( - self, - channel_id: Snowflake, - *, - before: str | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> ListThreadsResponse: - params = { - 'before': before, - 'limit': limit, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/threads/archived/public', - channel_id=channel_id, - ), - params=remove_undefined(params), - ) - - async def list_private_archived_threads( - self, - channel_id: Snowflake, - *, - before: str | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> ListThreadsResponse: - params = { - 'before': before, - 'limit': limit, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/threads/archived/private', - channel_id=channel_id, - ), - params=remove_undefined(params), - ) - - async def list_joined_private_archived_threads( - self, - channel_id: Snowflake, - *, - before: str | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> ListThreadsResponse: - params = { - 'before': before, - 'limit': limit, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/users/@me/threads/archived/private', - channel_id=channel_id, - ), - params=remove_undefined(params), - ) diff --git a/pycord/api/routers/emojis.py b/pycord/api/routers/emojis.py deleted file mode 100644 index 05b81ff5..00000000 --- a/pycord/api/routers/emojis.py +++ /dev/null @@ -1,105 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import Emoji -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class Emojis(BaseRouter): - async def list_guild_emojis(self, guild_id: Snowflake) -> list[Emoji]: - return await self.request( - 'GET', Route('/guilds/{guild_id}/emojis', guild_id=guild_id) - ) - - async def get_guild_emoji(self, guild_id: Snowflake, emoji_id: Snowflake) -> Emoji: - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/emojis/{emoji_id}', - guild_id=guild_id, - emoji_id=emoji_id, - ), - ) - - async def create_guild_emoji( - self, - guild_id: Snowflake, - *, - name: str, - image: bytes, # TODO - roles: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> Emoji: - payload = { - 'name': name, - 'image': image, - 'roles': roles, - } - return await self.request( - 'POST', - Route('POST', '/guilds/{guild_id}/emojis', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_guild_emoji( - self, - guild_id: Snowflake, - emoji_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - roles: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> Emoji: - payload = { - 'name': name, - 'roles': roles, - } - return await self.request( - 'PATCH', - Route( - '/guilds/{guild_id}/emojis/{emoji_id}', - guild_id=guild_id, - emoji_id=emoji_id, - ), - remove_undefined(**payload), - reason=reason, - ) - - async def delete_guild_emoji( - self, - guild_id: Snowflake, - emoji_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/emojis/{emoji_id}', - guild_id=guild_id, - emoji_id=emoji_id, - ), - reason=reason, - ) diff --git a/pycord/api/routers/guilds.py b/pycord/api/routers/guilds.py deleted file mode 100644 index e069c336..00000000 --- a/pycord/api/routers/guilds.py +++ /dev/null @@ -1,786 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -import datetime - -from ...file import File -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import ( - MFA_LEVEL, - WIDGET_STYLE, - Ban, - Channel, - DefaultReaction, - ForumTag, - Guild, - GuildMember, - GuildPreview, - Integration, - Invite, - ListThreadsResponse, - ModifyGuildChannelPositionsPayload, - ModifyGuildRolePositionsPayload, - PartialInvite, - Role, - VoiceRegion, - WelcomeScreen, - WelcomeScreenChannel, - Widget, - WidgetSettings, -) -from ...utils import remove_undefined, to_datauri -from ..route import Route -from .base import BaseRouter - - -class Guilds(BaseRouter): - async def create_guild( - self, - *, - name: str, - icon: File | MissingEnum = MISSING, - verification_level: int | MissingEnum = MISSING, - default_message_notifications: int | MissingEnum = MISSING, - explicit_content_filter: int | MissingEnum = MISSING, - roles: list[Role] | MissingEnum = MISSING, - channels: list[Channel] | MissingEnum = MISSING, - afk_channel_id: Snowflake | MissingEnum = MISSING, - afk_timeout: int | MissingEnum = MISSING, - system_channel_id: Snowflake | MissingEnum = MISSING, - system_channel_flags: int | MissingEnum = MISSING, - ) -> Guild: - payload = { - 'name': name, - 'icon': icon, - 'verification_level': verification_level, - 'default_message_notifications': default_message_notifications, - 'explicit_content_filter': explicit_content_filter, - 'roles': roles, - 'channels': channels, - 'afk_channel_id': afk_channel_id, - 'afk_timeout': afk_timeout, - 'system_channel_id': system_channel_id, - 'system_channel_flags': system_channel_flags, - } - - if payload.get('icon'): - payload['icon'] = to_datauri(payload['icon']) - - return await self.request('POST', Route('/guilds'), remove_undefined(**payload)) - - async def get_guild( - self, guild_id: Snowflake, *, with_counts: bool | MissingEnum = MISSING - ) -> Guild: - params = {'with_counts': with_counts} - return await self.request( - 'GET', - Route('/guilds/{guild_id}', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - - async def get_guild_preview(self, guild_id: Snowflake) -> GuildPreview: - return await self.request( - 'GET', Route('/guilds/{guild_id}/preview', guild_id=guild_id) - ) - - async def modify_guild( - self, - guild_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - verification_level: int | None | MissingEnum = MISSING, - default_message_notifications: int | None | MissingEnum = MISSING, - explicit_content_filter: int | None | MissingEnum = MISSING, - afk_channel_id: Snowflake | None | MissingEnum = MISSING, - afk_timeout: int | MissingEnum = MISSING, - icon: File | None | MissingEnum = MISSING, - owner_id: Snowflake | MissingEnum = MISSING, - splash: bytes | None | MissingEnum = MISSING, - discovery_splash: bytes | None | MissingEnum = MISSING, - banner: File | None | MissingEnum = MISSING, - system_channel_id: Snowflake | None | MissingEnum = MISSING, - system_channel_flags: int | MissingEnum = MISSING, - rules_channel_id: Snowflake | None | MissingEnum = MISSING, - public_updates_channel_id: Snowflake | None | MissingEnum = MISSING, - preferred_locale: str | None | MissingEnum = MISSING, - features: list[str] | MissingEnum = MISSING, - description: str | None | MissingEnum = MISSING, - premium_progress_bar_enabled: bool | MissingEnum = MISSING, - reason: str | None = None, - ) -> Guild: - payload = { - 'name': name, - 'verification_level': verification_level, - 'default_message_notifications': default_message_notifications, - 'explicit_content_filter': explicit_content_filter, - 'afk_channel_id': afk_channel_id, - 'afk_timeout': afk_timeout, - 'icon': icon, - 'owner_id': owner_id, - 'splash': splash, - 'discovery_splash': discovery_splash, - 'banner': banner, - 'system_channel_id': system_channel_id, - 'system_channel_flags': system_channel_flags, - 'rules_channel_id': rules_channel_id, - 'public_updates_channel_id': public_updates_channel_id, - 'preferred_locale': preferred_locale, - 'features': features, - 'description': description, - 'premium_progress_bar_enabled': premium_progress_bar_enabled, - } - - if payload.get('icon'): - payload['icon'] = to_datauri(payload['icon']) - if payload.get('banner'): - payload['banner'] = to_datauri(payload['banner']) - if payload.get('discovery_splash'): - payload['discovery_splash'] = to_datauri(payload['discovery_splash']) - if payload.get('splash'): - payload['splash'] = to_datauri(payload['splash']) - - return await self.request( - 'PATCH', - Route('/guilds/{guild_id}', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def delete_guild(self, guild_id: Snowflake) -> None: - await self.request('DELETE', Route('/guilds/{guild_id}', guild_id=guild_id)) - - async def get_guild_channels(self, guild_id: Snowflake) -> list[Channel]: - return await self.request( - 'GET', Route('/guilds/{guild_id}/channels', guild_id=guild_id) - ) - - async def create_guild_channel( - self, - guild_id: Snowflake, - *, - name: str, - type: int | None | MissingEnum = MISSING, - topic: str | None | MissingEnum = MISSING, - bitrate: int | None | MissingEnum = MISSING, - user_limit: int | None | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - permission_overwrites: list[dict] | None | MissingEnum = MISSING, - parent_id: Snowflake | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - rtc_region: str | None | MissingEnum = MISSING, - video_quality_mode: int | None | MissingEnum = MISSING, - default_auto_archive_duration: int | None | MissingEnum = MISSING, - default_reaction_emoji: DefaultReaction | None | MissingEnum = MISSING, - available_tags: list[ForumTag] | None | MissingEnum = MISSING, - default_sort_order: int | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> Channel: - payload = { - 'name': name, - 'type': type, - 'topic': topic, - 'bitrate': bitrate, - 'user_limit': user_limit, - 'rate_limit_per_user': rate_limit_per_user, - 'position': position, - 'permission_overwrites': permission_overwrites, - 'parent_id': parent_id, - 'nsfw': nsfw, - 'rtc_region': rtc_region, - 'video_quality_mode': video_quality_mode, - 'default_auto_archive_duration': default_auto_archive_duration, - 'default_reaction_emoji': default_reaction_emoji, - 'available_tags': available_tags, - 'default_sort_order': default_sort_order, - } - return await self.request( - 'POST', - Route('/guilds/{guild_id}/channels', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_guild_channel_positions( - self, guild_id: Snowflake, payload: list[ModifyGuildChannelPositionsPayload] - ) -> None: - await self.request( - 'PATCH', Route('/guilds/{guild_id}/channels', guild_id=guild_id), payload - ) - - async def list_active_guild_threads( - self, guild_id: Snowflake - ) -> list[ListThreadsResponse]: - return await self.request( - 'GET', Route('/guilds/{guild_id}/threads/active', guild_id=guild_id) - ) - - async def get_guild_member( - self, guild_id: Snowflake, user_id: Snowflake - ) -> GuildMember: - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/members/{user_id}', - guild_id=guild_id, - user_id=user_id, - ), - ) - - async def list_guild_members( - self, - guild_id: Snowflake, - *, - limit: int | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - ) -> list[GuildMember]: - params = { - 'limit': limit, - 'after': str(after) if after is not MISSING else MISSING, - } - return await self.request( - 'GET', - Route('/guilds/{guild_id}/members', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - - async def search_guild_members( - self, - guild_id: Snowflake, - *, - query: str, - limit: int | None | MissingEnum = MISSING, - ) -> list[GuildMember]: - params = { - 'query': query, - 'limit': limit, - } - return await self.request( - 'GET', - Route('/guilds/{guild_id}/members/search', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - - async def add_guild_member( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - access_token: str, - nick: str | MissingEnum = MISSING, - roles: list[Snowflake] | MissingEnum = MISSING, - mute: bool | MissingEnum = MISSING, - deaf: bool | MissingEnum = MISSING, - ) -> GuildMember: - payload = { - 'access_token': access_token, - 'nick': nick, - 'roles': roles, - 'mute': mute, - 'deaf': deaf, - } - return await self.request( - 'PUT', - Route( - '/guilds/{guild_id}/members/{user_id}', - guild_id=guild_id, - user_id=user_id, - ), - remove_undefined(**payload), - ) - - async def modify_guild_member( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - nick: str | None | MissingEnum = MISSING, - roles: list[Snowflake] | None | MissingEnum = MISSING, - mute: bool | None | MissingEnum = MISSING, - deaf: bool | None | MissingEnum = MISSING, - channel_id: Snowflake | None | MissingEnum = MISSING, - communication_disabled_until: datetime.datetime | None | MissingEnum = MISSING, - flags: int | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> GuildMember: - if communication_disabled_until: - communication_disabled_until = communication_disabled_until.isoformat() - payload = { - 'nick': nick, - 'roles': roles, - 'mute': mute, - 'deaf': deaf, - 'channel_id': channel_id, - 'communication_disabled_until': communication_disabled_until, - 'flags': flags, - } - return await self.request( - 'PATCH', - Route( - '/guilds/{guild_id}/members/{user_id}', - guild_id=guild_id, - user_id=user_id, - ), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_current_member( - self, - guild_id: Snowflake, - *, - nick: str | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> GuildMember: - payload = { - 'nick': nick, - } - return await self.request( - 'PATCH', - Route('/guilds/{guild_id}/members/@me', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def add_guild_member_role( - self, - guild_id: Snowflake, - user_id: Snowflake, - role_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'PUT', - Route( - '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', - guild_id=guild_id, - user_id=user_id, - role_id=role_id, - ), - reason=reason, - ) - - async def remove_guild_member_role( - self, - guild_id: Snowflake, - user_id: Snowflake, - role_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', - guild_id=guild_id, - user_id=user_id, - role_id=role_id, - ), - reason=reason, - ) - - async def remove_guild_member( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/members/{user_id}', - guild_id=guild_id, - user_id=user_id, - ), - reason=reason, - ) - - async def get_guild_bans( - self, - guild_id: Snowflake, - *, - limit: int | MissingEnum = MISSING, - before: Snowflake | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - ) -> list[Ban]: - params = { - 'limit': limit, - 'before': before, - 'after': after, - } - return await self.request( - 'GET', - Route('/guilds/{guild_id}/bans', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - - async def get_guild_ban(self, guild_id: Snowflake, user_id: Snowflake) -> Ban: - return await self.request( - 'GET', - Route( - '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id - ), - ) - - async def create_guild_ban( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - delete_message_seconds: int | MissingEnum = MISSING, - reason: str | None = None, - ) -> None: - payload = { - 'delete_message_seconds': delete_message_seconds, - } - await self.request( - 'PUT', - Route( - '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id - ), - remove_undefined(**payload), - reason=reason, - ) - - async def remove_guild_ban( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id - ), - reason=reason, - ) - - async def get_guild_roles(self, guild_id: Snowflake) -> list[Role]: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/roles', guild_id=guild_id), - ) - - async def create_guild_role( - self, - guild_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - permissions: int | MissingEnum = MISSING, - color: int | MissingEnum = MISSING, - hoist: bool | MissingEnum = MISSING, - icon: bytes | None | MissingEnum = MISSING, # TODO - unicode_emoji: str | None | MissingEnum = MISSING, - mentionable: bool | MissingEnum = MISSING, - reason: str | None = None, - ) -> Role: - payload = { - 'name': name, - 'permissions': str(permissions), - 'color': color, - 'hoist': hoist, - 'unicode_emoji': unicode_emoji, - 'mentionable': mentionable, - } - return await self.request( - 'POST', - Route('/guilds/{guild_id}/roles', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_guild_role_positions( - self, - guild_id: Snowflake, - role_positions: list[ModifyGuildRolePositionsPayload], - *, - reason: str | None = None, - ) -> list[Role]: - return await self.request( - 'PATCH', - Route('/guilds/{guild_id}/roles', guild_id=guild_id), - role_positions, - reason=reason, - ) - - async def modify_guild_role( - self, - guild_id: Snowflake, - role_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - permissions: int | MissingEnum = MISSING, - color: int | MissingEnum = MISSING, - hoist: bool | MissingEnum = MISSING, - icon: bytes | None | MissingEnum = MISSING, # TODO - unicode_emoji: str | None | MissingEnum = MISSING, - mentionable: bool | MissingEnum = MISSING, - reason: str | None = None, - ) -> Role: - payload = { - 'name': name, - 'permissions': str(permissions), - 'color': color, - 'hoist': hoist, - 'unicode_emoji': unicode_emoji, - 'mentionable': mentionable, - } - return await self.request( - 'PATCH', - Route( - '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id - ), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_guild_mfa_level( - self, - guild_id: Snowflake, - level: MFA_LEVEL, - *, - reason: str | None = None, - ) -> MFA_LEVEL: - payload = { - 'level': level, - } - response = await self.request( - 'PATCH', - Route('/guilds/{guild_id}', guild_id=guild_id), - payload, - reason=reason, - ) - return response['level'] - - async def delete_guild_role( - self, - guild_id: Snowflake, - role_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id - ), - reason=reason, - ) - - async def get_guild_prune_count( - self, - guild_id: Snowflake, - *, - days: int | MissingEnum = MISSING, - include_roles: list[Snowflake] | MissingEnum = MISSING, - ) -> int: - params = { - 'days': days, - 'include_roles': include_roles, - } - response = await self.request( - 'GET', - Route('/guilds/{guild_id}/prune', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - return response['pruned'] - - async def begin_guild_prune( - self, - guild_id: Snowflake, - *, - days: int | MissingEnum = MISSING, - compute_prune_count: bool | MissingEnum = MISSING, - include_roles: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> int: - payload = { - 'days': days, - 'compute_prune_count': compute_prune_count, - 'include_roles': include_roles, - } - response = await self.request( - 'POST', - Route('/guilds/{guild_id}/prune', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - return response['pruned'] - - async def get_guild_voice_regions(self, guild_id: Snowflake) -> list[VoiceRegion]: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/regions', guild_id=guild_id), - ) - - async def get_guild_invites(self, guild_id: Snowflake) -> list[Invite]: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/invites', guild_id=guild_id), - ) - - async def get_guild_integrations(self, guild_id: Snowflake) -> list[Integration]: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/integrations', guild_id=guild_id), - ) - - async def delete_guild_integration( - self, - guild_id: Snowflake, - integration_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/guilds/{guild_id}/integrations/{integration_id}', - guild_id=guild_id, - integration_id=integration_id, - ), - reason=reason, - ) - - async def get_guild_widget_settings(self, guild_id: Snowflake) -> WidgetSettings: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/widget', guild_id=guild_id), - ) - - async def modify_guild_widget( - self, - guild_id: Snowflake, - *, - enabled: bool | MissingEnum = MISSING, - channel_id: Snowflake | MissingEnum = MISSING, - reason: str | None = None, - ) -> WidgetSettings: - payload = { - 'enabled': enabled, - 'channel_id': channel_id, - } - return await self.request( - 'PATCH', - Route('/guilds/{guild_id}/widget', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def get_guild_widget(self, guild_id: Snowflake) -> Widget: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/widget.json', guild_id=guild_id), - ) - - async def get_guild_vanity_url(self, guild_id: Snowflake) -> PartialInvite: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/vanity-url', guild_id=guild_id), - ) - - async def get_guild_widget_image( - self, guild_id: Snowflake, style: WIDGET_STYLE | MissingEnum = MISSING - ) -> bytes: - params = { - 'style': style, - } - return await self.request( - 'GET', - Route('/guilds/{guild_id}/widget.png', guild_id=guild_id), - query_params=remove_undefined(**params), - ) - - async def get_guild_welcome_screen(self, guild_id: Snowflake) -> WelcomeScreen: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/welcome-screen', guild_id=guild_id), - ) - - async def modify_guild_welcome_screen( - self, - guild_id: Snowflake, - *, - enabled: bool | None | MissingEnum = MISSING, - welcome_channels: list[WelcomeScreenChannel] | None | MissingEnum = MISSING, - description: str | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> WelcomeScreen: - payload = { - 'enabled': enabled, - 'welcome_channels': welcome_channels, - 'description': description, - } - return await self.request( - 'PATCH', - Route('/guilds/{guild_id}/welcome-screen', guild_id=guild_id), - remove_undefined(**payload), - reason=reason, - ) - - async def modify_current_user_voice_state( - self, - guild_id: Snowflake, - *, - channel_id: Snowflake | MissingEnum = MISSING, - suppress: bool | MissingEnum = MISSING, - request_to_speak_timestamp: datetime.datetime | None | MissingEnum = MISSING, - ) -> None: - if request_to_speak_timestamp: - request_to_speak_timestamp = request_to_speak_timestamp.isoformat() - payload = { - 'channel_id': channel_id, - 'suppress': suppress, - 'request_to_speak_timestamp': request_to_speak_timestamp, - } - await self.request( - 'PATCH', - Route('/guilds/{guild_id}/voice-states/@me', guild_id=guild_id), - remove_undefined(**payload), - ) - - async def modify_user_voice_state( - self, - guild_id: Snowflake, - user_id: Snowflake, - *, - channel_id: Snowflake | MissingEnum = MISSING, - suppress: bool | MissingEnum = MISSING, - reason: str | None = None, - ) -> None: - payload = { - 'channel_id': channel_id, - 'suppress': suppress, - } - await self.request( - 'PATCH', - Route( - '/guilds/{guild_id}/voice-states/{user_id}', - guild_id=guild_id, - user_id=user_id, - ), - remove_undefined(**payload), - reason=reason, - ) diff --git a/pycord/api/routers/messages.py b/pycord/api/routers/messages.py deleted file mode 100644 index eda89da1..00000000 --- a/pycord/api/routers/messages.py +++ /dev/null @@ -1,296 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from urllib.parse import quote - -from ...file import File -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import Attachment, Emoji, User -from ...types.channel import AllowedMentions -from ...types.component import Component -from ...types.embed import Embed -from ...types.message import Message, MessageReference -from ...utils import remove_undefined -from ..route import Route -from .base import BaseRouter - - -class Messages(BaseRouter): - async def create_message( - self, - channel_id: Snowflake, - *, - content: str | MissingEnum = MISSING, - nonce: int | str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - allowed_mentions: AllowedMentions | MissingEnum = MISSING, - message_reference: MessageReference | MissingEnum = MISSING, - components: list[Component] | MissingEnum = MISSING, - sticker_ids: list[Snowflake] | MissingEnum = MISSING, - files: list[File] | MissingEnum = MISSING, - flags: int | MissingEnum = MISSING, - ) -> Message: - data = { - 'content': content, - 'nonce': nonce, - 'tts': tts, - 'embeds': embeds, - 'allowed_mentions': allowed_mentions, - 'message_reference': message_reference, - 'components': components, - 'sticker_ids': sticker_ids, - 'flags': flags, - } - - return await self.request( - 'POST', - Route('/channels/{channel_id}/messages', channel_id=channel_id), - remove_undefined(**data), - files=files, - ) - - async def crosspost_message( - self, channel_id: Snowflake, message_id: Snowflake - ) -> Message: - return await self.request( - 'POST', - Route( - '/channels/{channel_id}/messages/{message_id}/crosspost', - channel_id=channel_id, - message_id=message_id, - ), - ) - - async def create_reaction( - self, - channel_id: Snowflake, - message_id: Snowflake, - emoji: str | Emoji, - ) -> None: - if isinstance(emoji, Emoji): - emoji = f'{emoji.name}:{emoji.id}' - emoji = quote(emoji) - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', - channel_id=channel_id, - message_id=message_id, - emoji=emoji, - ), - ) - - async def delete_own_reaction( - self, - channel_id: Snowflake, - message_id: Snowflake, - emoji: str | Emoji, - ) -> None: - if isinstance(emoji, Emoji): - emoji = f'{emoji.name}:{emoji.id}' - emoji = quote(emoji) - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', - channel_id=channel_id, - message_id=message_id, - emoji=emoji, - ), - ) - - async def delete_user_reaction( - self, - channel_id: Snowflake, - message_id: Snowflake, - emoji: str | Emoji, - user_id: Snowflake, - ) -> None: - if isinstance(emoji, Emoji): - emoji = f'{emoji.name}:{emoji.id}' - emoji = quote(emoji) - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{user_id}', - channel_id=channel_id, - message_id=message_id, - emoji=emoji, - user_id=user_id, - ), - ) - - async def get_reactions( - self, - channel_id: Snowflake, - message_id: Snowflake, - emoji: str | Emoji, - *, - limit: int | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - ) -> list[User]: - if isinstance(emoji, Emoji): - emoji = f'{emoji.name}:{emoji.id}' - emoji = quote(emoji) - params = { - 'limit': limit, - 'after': after, - } - return await self.request( - 'GET', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', - channel_id=channel_id, - message_id=message_id, - emoji=emoji, - ), - query_params=remove_undefined(**params), - ) - - async def delete_all_reactions( - self, - channel_id: Snowflake, - message_id: Snowflake, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions', - channel_id=channel_id, - message_id=message_id, - ), - ) - - async def delete_all_reactions_for_emoji( - self, - channel_id: Snowflake, - message_id: Snowflake, - emoji: str | Emoji, - ) -> None: - if isinstance(emoji, Emoji): - emoji = f'{emoji.name}:{emoji.id}' - emoji = quote(emoji) - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', - channel_id=channel_id, - message_id=message_id, - emoji=emoji, - ), - ) - - async def edit_message( - self, - channel_id: Snowflake, - message_id: Snowflake, - *, - content: str | None | MissingEnum = MISSING, - embeds: list[Embed] | None | MissingEnum = MISSING, - flags: int | None | MissingEnum = MISSING, - allowed_mentions: AllowedMentions | None | MissingEnum = MISSING, - components: list[Component] | None | MissingEnum = MISSING, - files: list[File] | None | MissingEnum = MISSING, - attachments: list[Attachment] | None | MissingEnum = MISSING, - ) -> Message: - data = { - 'content': content, - 'embeds': embeds, - 'flags': flags, - 'allowed_mentions': allowed_mentions, - 'components': components, - 'attachments': attachments, - } - return await self.request( - 'PATCH', - Route( - '/channels/{channel_id}/messages/{message_id}', - channel_id=channel_id, - message_id=message_id, - ), - remove_undefined(**data), - files=files, - ) - - async def delete_message( - self, - channel_id: Snowflake, - message_id: Snowflake, - *, - reason: str | None = None, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/messages/{message_id}', - channel_id=channel_id, - message_id=message_id, - ), - reason=reason, - ) - - async def bulk_delete_messages( - self, - channel_id: Snowflake, - message_ids: list[Snowflake], - *, - reason: str | None = None, - ) -> None: - await self.request( - 'POST', - Route( - '/channels/{channel_id}/messages/bulk-delete', - channel_id=channel_id, - ), - { - 'messages': message_ids, - }, - reason=reason, - ) - - async def pin_message( - self, - channel_id: Snowflake, - message_id: Snowflake, - ) -> None: - await self.request( - 'PUT', - Route( - '/channels/{channel_id}/pins/{message_id}', - channel_id=channel_id, - message_id=message_id, - ), - ) - - async def unpin_message( - self, - channel_id: Snowflake, - message_id: Snowflake, - ) -> None: - await self.request( - 'DELETE', - Route( - '/channels/{channel_id}/pins/{message_id}', - channel_id=channel_id, - message_id=message_id, - ), - ) diff --git a/pycord/api/routers/scheduled_events.py b/pycord/api/routers/scheduled_events.py deleted file mode 100644 index b9f002bf..00000000 --- a/pycord/api/routers/scheduled_events.py +++ /dev/null @@ -1,73 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from ...file import File -from ...missing import MISSING, MissingEnum -from ...snowflake import Snowflake -from ...types import PRIVACY_LEVEL, EntityMetadata, GuildScheduledEvent -from ...utils import remove_undefined, to_datauri -from ..route import Route -from .base import BaseRouter - - -class ScheduledEvents(BaseRouter): - async def list_scheduled_events( - self, guild_id: Snowflake, with_user_count: bool | MissingEnum = MISSING - ) -> list[GuildScheduledEvent]: - return await self.request( - 'GET', - Route('/guilds/{guild_id}/scheduled-events', guild_id=guild_id), - remove_undefined(with_user_count=with_user_count), - ) - - async def create_guild_scheduled_event( - self, - guild_id: Snowflake, - name: str, - scheduled_start_time: str, - entity_type: Literal[1, 2, 3], - channel_id: Snowflake | MissingEnum = MISSING, - entity_metadata: EntityMetadata | MissingEnum = MISSING, - privacy_level: PRIVACY_LEVEL | MissingEnum = MISSING, - scheduled_end_time: str | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - image: File | MissingEnum = MISSING, - ) -> GuildScheduledEvent: - fields = remove_undefined( - name=name, - scheduled_start_time=scheduled_start_time, - entity_type=entity_type, - channel_id=channel_id, - entity_metadata=entity_metadata, - privacy_level=privacy_level, - scheduled_end_time=scheduled_end_time, - description=description, - image=image, - ) - - if fields.get('image'): - fields['image'] = to_datauri(fields['image']) - - # TODO - await self.request( - 'POST', - ) diff --git a/pycord/api/routers/user.py b/pycord/api/routers/user.py deleted file mode 100644 index 89aa3539..00000000 --- a/pycord/api/routers/user.py +++ /dev/null @@ -1,30 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -from ...types import User -from ..route import Route -from .base import BaseRouter - - -class Users(BaseRouter): - async def get_current_user(self) -> User: - return await self.request('GET', Route('/users/@me')) diff --git a/pycord/application.py b/pycord/application.py index 1b8641a0..6859063f 100644 --- a/pycord/application.py +++ b/pycord/application.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,149 +18,341 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations -from typing import TYPE_CHECKING +from __future__ import annotations +from .asset import Asset +from .enums import MembershipState from .flags import ApplicationFlags, Permissions -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .team import Team -from .types import ( - SCOPE, - Application as DiscordApplication, - InstallParams as DiscordInstallParams, -) +from .guild import Guild +from .mixins import Identifiable +from .missing import Maybe, MISSING from .user import User +from typing import TYPE_CHECKING + if TYPE_CHECKING: + from discord_typings import ( + ApplicationData, PartialApplicationData, InstallParams as InstallParamsData, + OAuth2Scopes, TeamData, TeamMemberData, + ) + from .file import File from .state import State +__all__ = ( + "Application", + "InstallParams", + "Team", + "TeamMember", +) + + +class Application(Identifiable): + """ + Represents a Discord application. + + Attributes + ----------- + id: :class:`int` + The application's ID. + name: :class:`str` | :class:`MISSING` + The application's name. + icon_hash: :class:`str` | None | :class:`MISSING` + The application's icon hash. + description: :class:`str` | :class:`MISSING` + The application's description. + rpc_origins: list[:class:`str`] | :class:`MISSING` + The application's RPC origins. + bot_public: :class:`bool` | :class:`MISSING` + Whether the bot is public. + bot_require_code_grant: :class:`bool | :class:`MISSING` + Whether the bot requires a code grant before being added to the server. + bot: :class:`User` | :class:`MISSING` + The bot's user object. + terms_of_service_url: :class:`str | :class:`MISSING` + The application's terms of service URL. + privacy_policy_url: :class:`str` | :class:`MISSING` + The application's privacy policy URL. + owner: :class:`User` | :class:`MISSING` + The application's owner. + verify_key: :class:`str` | :class:`MISSING` + The application's verification key. + team: :class:`Team` | None + The team the application is part of. + guild_id: :class:`int` | :class:`MISSING` + The guild ID the application is part of. + guild: :class:`Guild` | :class:`MISSING` + The guild the application is part of. + primary_sku_id: :class:`int` | :class:`MISSING` + The ID of the application's primary SKU. + slug: :class:`str` | :class:`MISSING` + The application's slug. + cover_image_hash: :class:`str` | :class:`MISSING` + The application's cover image hash. + flags: :class:`ApplicationFlags` | :class:`MISSING` + The application's flags. + approximate_guild_count: :class:`int` | :class:`MISSING` + The approximate guild count for the application. + redirect_uris: list[:class:`str`] | :class:`MISSING` + The application's redirect URIs. + role_connections_verification_url: :class:`str` | :class:`MISSING` + The application's role connection verification URL. + tags: list[:class:`str`] | :class:`MISSING` + The application's tags. + install_params: :class:`InstallParams` | :class:`MISSING` + The application's install parameters. + """ + __slots__ = ( + "_state", + "id", + "name", + "icon_hash", + "description", + "rpc_origins", + "bot_public", + "bot_require_code_grant", + "bot", + "terms_of_service_url", + "privacy_policy_url", + "owner", + "verify_key", + "team", + "guild_id", + "guild", + "primary_sku_id", + "slug", + "cover_image_hash", + "flags", + "approximate_guild_count", + "redirect_uris", + "role_connections_verification_url", + "tags", + "install_params", + "custom_install_url", + ) + + def __init__(self, data: ApplicationData | PartialApplicationData, state: State) -> None: + self._state: State = state + self.id: int = int(data["id"]) + self.name: Maybe[str] = data.get("name", MISSING) + self.icon_hash: Maybe[str | None] = data.get("icon", MISSING) + self.description: Maybe[str] = data.get("description", MISSING) + self.rpc_origins: Maybe[list[str]] = data.get("rpc_origins", MISSING) + self.bot_public: Maybe[bool] = data.get("bot_public", MISSING) + self.bot_require_code_grant: Maybe[bool] = data.get("bot_require_code_grant", MISSING) + self.bot: Maybe[User] = User(data["bot"], state) if "bot" in data else MISSING + self.terms_of_service_url: Maybe[str] = data.get("terms_of_service_url", MISSING) + self.privacy_policy_url: Maybe[str] = data.get("privacy_policy_url", MISSING) + self.owner: Maybe[User] = User(data["owner"], state) if "owner" in data else MISSING + self.verify_key: Maybe[str] = data.get("verify_key", MISSING) + self.team: Team | None = Team(teamdata, state) if (teamdata := data.get("team")) else None + self.guild_id: Maybe[int] = data.get("guild_id", MISSING) + self.guild: Maybe[Guild] = Guild(guild, state) if (guild := data.get("guild")) else MISSING + self.primary_sku_id: Maybe[int] = data.get("primary_sku_id", MISSING) + self.slug: Maybe[str] = data.get("slug", MISSING) + self.cover_image_hash: Maybe[str] = data.get("cover_image", MISSING) + self.flags: Maybe[ApplicationFlags] = ApplicationFlags.from_value(data["flags"]) if "flags" in data else MISSING + self.approximate_guild_count: Maybe[int] = data.get("approximate_guild_count", MISSING) + self.redirect_uris: Maybe[list[str]] = data.get("redirect_uris", MISSING) + self.role_connections_verification_url: Maybe[str] = data.get("role_connections_verification_url", MISSING) + self.tags: Maybe[list[str]] = data.get("tags", MISSING) + self.install_params: Maybe[InstallParams] = InstallParams.from_data( + data["install_params"] + ) if "install_params" in data else MISSING + self.custom_install_url: Maybe[str] = data.get("custom_install_url", MISSING) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.__repr__() + + @property + def icon(self) -> Asset | None: + """:class:`Asset` | None: The application's icon, if it has one.""" + return Asset.from_application_icon(self._state, self.id, self.icon_hash) if self.icon_hash else None + + @property + def cover_image(self) -> Asset | None: + """:class:`Asset` | None: The application's cover image, if it has one.""" + return Asset.from_application_cover( + self._state, self.id, self.cover_image_hash + ) if self.cover_image_hash else None + + # TODO: move to client + async def edit( + self, + custom_install_url: Maybe[str] = MISSING, + description: Maybe[str] = MISSING, + role_connections_verification_url: Maybe[str] = MISSING, + install_params: Maybe[InstallParams] = MISSING, + flags: Maybe[ApplicationFlags] = MISSING, + icon: Maybe[File] = MISSING, + cover_image: Maybe[File] = MISSING, + interactions_endpoint_url: Maybe[str] = MISSING, + tags: Maybe[list[str]] = MISSING, + ) -> Application: + """ + Edits the application. + + All parameters for this method are optional. + + Parameters + ----------- + custom_install_url: :class:`str` + The new custom install URL. + description: :class:`str` + The new description. + role_connections_verification_url: :class:`str` + The new role connections verification URL. + install_params: :class:`InstallParams` + The new install parameters. + flags: :class:`ApplicationFlags` + The new flags. + icon: :class:`File` + The new icon. + cover_image: :class:`File` + The new cover image. + interactions_endpoint_url: :class:`str` + The new interactions endpoint URL. + tags: list[:class:`str`] + The new tags. + + Returns + -------- + :class:`Application` + The edited application. + + Raises + ------- + :exc:`HTTPException` + Editing the application failed. + """ + payload = { + "custom_install_url": custom_install_url, + "description": description, + "role_connections_verification_url": role_connections_verification_url, + "install_params": install_params, + "flags": flags.as_bit if flags is not MISSING else MISSING, + "icon": icon, + "cover_image": cover_image, + "interactions_endpoint_url": interactions_endpoint_url, + "tags": tags, + } + data = await self._state.http.edit_current_application(**payload) + return Application(data, self._state) + class InstallParams: """ - Discord's Application Installation Parameters. + Represents the default installation parameters for an application. Attributes - ---------- - scopes: list[:class:`.types.SCOPE`] - permissions: :class:`.flags.Permissions` + ----------- + scopes: list[:class:`str`] + The default OAuth2 scopes. + permissions: :class:`Permissions` + The default permissions requested. """ + __slots__ = ( + "scopes", + "permissions", + ) - __slots__ = ('scopes', 'permissions') + def __init__(self, *, scopes: list[OAuth2Scopes], permissions: Permissions) -> None: + self.scopes: list[OAuth2Scopes] = scopes + self.permissions: Permissions = permissions - def __init__(self, data: DiscordInstallParams) -> None: - self.scopes: list[SCOPE] = data['scopes'] - self.permissions: Permissions = Permissions.from_value(data['permissions']) + def __repr__(self) -> str: + return f"" + def __str__(self) -> str: + return self.__repr__() -class Application: + @classmethod + def from_data(cls, data: InstallParamsData) -> InstallParams: + return cls( + scopes=data["scopes"], + permissions=Permissions.from_value(data["permissions"]), + ) + + +class Team(Identifiable): """ - Represents a Discord Application. Like a bot or webhook. + Represents a Discord team. Attributes - ---------- - id: :class:`.snowflake.Snowflake` + ----------- + id: :class:`int` + The team's ID. name: :class:`str` - Name of this Application - icon: :class:`str` - The Icon hash of the Application - description: :class:`str` - Description of this Application - rpc_origins: list[:class:`str`] | :class:`.undefined.MissingEnum` - A list of RPC Origins for this Application - bot_public: :class:`bool` - Whether this bot can be invited by anyone or only the owner - bot_require_code_grant: :class:`bool` - Whether this bot needs a code grant to be invited - terms_of_service_url: :class:`str` | :class:`.undefined.MissingEnum` - The TOS url of this Application - privacy_policy_url: :class:`str` | :class:`.undefined.MissingEnum` - The Privacy Policy url of this Application - owner: :class:`.user.User` | :class:`.undefined.MissingEnum` - The owner of this application, if any, or only if a user - verify_key: :class:`str` - The verification key of this Application - team: :class:`Team` | None - The team of this Application, if any - guild_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - The guild this application is withheld in, if any - primary_sku_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - The Primary SKU ID (Product ID) of this Application, if any - slug: :class:`str` | :class:`.undefined.MissingEnum` - The slug of this Application, if any - flags: :class:`.flags.ApplicationFlags` | :class:`.undefined.MissingEnum` - A Class representation of this Application's Flags - tags: list[:class:`str`] - The list of tags this Application withholds - install_params: :class:`.application.InstallParams` | :class:`.undefined.MissingEnum` - custom_install_url: :class:`str` | :class:`.undefined.MissingEnum` - The Custom Installation URL of this Application + The team's name. + icon_hash: :class:`str` | None | :class:`MISSING` + The team's icon hash. + members: list[:class:`TeamMember`] + The team's members. + owner_user_id: :class:`int` + The team's owner's user ID. """ + __slots__ = ( + "_state", + "icon_hash", + "id", + "members", + "name", + "owner_user_id", + ) + + def __init__(self, data: TeamData, state: State) -> None: + self._state: State = state + self.icon_hash: str | None = data["icon"] + self.id: int = int(data["id"]) + self.members: list[TeamMember] = [TeamMember(member, state) for member in data["members"]] + self.name: str = data["name"] + self.owner_user_id: int = int(data["owner_user_id"]) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + @property + def icon(self) -> Asset | None: + """Optional[:class:`Asset`]: The team's icon, if it has one.""" + return Asset.from_team_icon(self._state, self.id, self.icon_hash) if self.icon_hash else None + +class TeamMember(Identifiable): + """ + Represents a member of a Discord team. + + Attributes + ----------- + membership_state: :class:`MembershipState` + The member's membership state (e.g. invited, accepted). + team_id: :class:`int` + The team's ID. + user: :class:`User` + The member's user object. + role: :class:`str` + The member's role in the team. + """ __slots__ = ( - '_cover_image', - 'id', - 'name', - 'icon', - 'description', - 'rpc_origins', - 'bot_public', - 'bot_require_code_grant', - 'terms_of_service_url', - 'privacy_policy_url', - 'owner', - 'verify_key', - 'team', - 'guild_id', - 'primary_sku_id', - 'slug', - 'flags', - 'tags', - 'install_params', - 'custom_install_url', + "membership_state", + "team_id", + "user", + "role", ) - def __init__(self, data: DiscordApplication, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.icon: str | None = data['icon'] - self.description: str = data['description'] - self.rpc_origins: Maybe[list[str]] = data.get('rpc_origins', MISSING) - self.bot_public: bool = data['bot_public'] - self.bot_require_code_grant: bool = data['bot_require_code_grant'] - self.terms_of_service_url: Maybe[str] = data.get( - 'terms_of_service_url', MISSING - ) - self.privacy_policy_url: Maybe[str] = data.get('privacy_policy_url', MISSING) - self.owner: Maybe[User] = ( - User(data.get('owner'), state) if data.get('owner') is not None else MISSING - ) - self.verify_key: str = data.get('verify_key') - self.team: Team | None = ( - Team(data.get('team')) if data.get('team') is not None else None - ) - self.guild_id: Maybe[Snowflake] = ( - Snowflake(data.get('guild_id')) - if data.get('guild_id') is not None - else MISSING - ) - self.primary_sku_id: Maybe[Snowflake] = ( - Snowflake(data.get('primary_sku_id')) - if data.get('primary_sku_id') is not None - else MISSING - ) - self.slug: Maybe[str] = data.get('slug', MISSING) - self._cover_image: Maybe[str] = data.get('cover_image', MISSING) - self.flags: Maybe[ApplicationFlags] = ( - ApplicationFlags.from_value(data.get('flags')) - if data.get('flags') is not None - else MISSING - ) - self.tags: list[str] = data.get('tags', []) - self.install_params: InstallParams | MissingEnum = ( - InstallParams(data.get('install_params')) - if data.get('install_params') is not None - else MISSING - ) - self.custom_install_url: str | MissingEnum = data.get('custom_install_url') + def __init__(self, data: TeamMemberData, state: State) -> None: + self.membership_state: MembershipState = MembershipState(data["membership_state"]) + self.team_id: int = int(data["team_id"]) + self.user: User = User(data["user"], state) + self.role: str = data["role"] + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.user) diff --git a/pycord/application_role_connection.py b/pycord/application_role_connection.py new file mode 100644 index 00000000..efffeb32 --- /dev/null +++ b/pycord/application_role_connection.py @@ -0,0 +1,92 @@ +# cython: language_level=3 +# Copyright (c) 2022-present Pycord Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE + +from __future__ import annotations + +from .enums import ApplicationRoleConnectionMetadataType +from .missing import Maybe, MISSING + +from typing import cast, TYPE_CHECKING + +if TYPE_CHECKING: + from discord_typings import ApplicationRoleConnectionMetadataData, Locales + +__all__ = ( + "ApplicationRoleConnectionMetadata", +) + + +class ApplicationRoleConnectionMetadata: + __slots__ = ( + "type", + "key", + "name", + "description", + "name_localizations", + "description_localizations", + ) + + def __init__( + self, + type: ApplicationRoleConnectionMetadataType, + *, + key: str, + name: str, + description: str, + name_localizations: Maybe[dict[Locales, str]] = MISSING, + description_localizations: Maybe[dict[Locales, str]] = MISSING, + ) -> None: + self.type: ApplicationRoleConnectionMetadataType = type + self.key: str = key + self.name: str = name + self.description: str = description + self.name_localizations: Maybe[dict[Locales, str]] = name_localizations + self.description_localizations: Maybe[dict[Locales, str]] = description_localizations + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + @classmethod + def from_data(cls, data: ApplicationRoleConnectionMetadataData) -> ApplicationRoleConnectionMetadata: + return cls( + ApplicationRoleConnectionMetadataType(data["type"]), + key=data["key"], + name=data["name"], + description=data["description"], + name_localizations=data.get("name_localizations", MISSING), + description_localizations=data.get("description_localizations", MISSING), + ) + + def to_data(self) -> ApplicationRoleConnectionMetadataData: + payload: ApplicationRoleConnectionMetadataData = { + "type": self.type.value, + "key": self.key, + "name": self.name, + "description": self.description, + } + if self.name_localizations is not MISSING: + payload["name_localizations"] = cast(dict[Locales, str], self.name_localizations) + if self.description_localizations is not MISSING: + payload["description_localizations"] = cast(dict[Locales, str], self.description_localizations) + return payload diff --git a/pycord/application_role_connection_metadata.py b/pycord/application_role_connection_metadata.py deleted file mode 100644 index ec6a30db..00000000 --- a/pycord/application_role_connection_metadata.py +++ /dev/null @@ -1,103 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import Sequence - -from .enums import ApplicationRoleConnectionMetadataType -from .missing import MISSING, Maybe, MissingEnum -from .types import ( - ApplicationRoleConnectionMetadata as DiscordApplicationRoleConnectionMetadata, -) -from .user import LOCALE -from .utils import remove_undefined - -__all__: Sequence[str] = ('ApplicationRoleConnectionMetadata',) - - -class ApplicationRoleConnectionMetadata: - """Represents a Discord Application's Role Connection Metadata. - - Attributes - ---------- - type: :class:`ApplicationRoleConnectionMetadataType` - The type of the role connection metadata. - key: :class:`str` - The key for the role connection metadata field. - name: :class:`str` - The name of the role connection metadata field. - description: :class:`str` - The description of the role connection metadata field. - name_localizations: :class:`dict`\\[:class:`str`, :class:`str`] - The localizations for the name of the role connection metadata field. - description_localizations: :class:`dict[str, str]` - The localizations for the description of the role connection metadata field. - """ - - __slots__ = ( - 'type', - 'key', - 'name', - 'description', - 'name_localizations', - 'description_localizations', - ) - - def __init__( - self, - *, - type: ApplicationRoleConnectionMetadataType, - key: str, - name: str, - description: str, - name_localizations: Maybe[dict[LOCALE, str]] = MISSING, - description_localizations: Maybe[dict[LOCALE, str]] = MISSING, - ) -> None: - self.type: ApplicationRoleConnectionMetadataType = type - self.key: str = key - self.name: str = name - self.description: str = description - self.name_localizations: dict[LOCALE, str] = name_localizations - self.description_localizations: dict[LOCALE, str] = description_localizations - - def __repr__(self) -> str: - return ( - f'' - ) - - @classmethod - def from_dict(cls, data: dict[str, str]) -> ApplicationRoleConnectionMetadata: - type = ApplicationRoleConnectionMetadataType(data.pop('type')) - return cls(type=type, **data) - - def to_dict(self) -> DiscordApplicationRoleConnectionMetadata: - return remove_undefined( - **{ - 'type': self.type.value, - 'key': self.key, - 'name': self.name, - 'description': self.description, - 'name_localizations': self.name_localizations, - 'description_localizations': self.description_localizations, - } - ) diff --git a/pycord/asset.py b/pycord/asset.py new file mode 100644 index 00000000..16782b6f --- /dev/null +++ b/pycord/asset.py @@ -0,0 +1,244 @@ +# cython: language_level=3 +# Copyright (c) 2022-present Pycord Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE + + +from typing import Literal, TYPE_CHECKING, Self +from urllib.parse import parse_qs, urlparse + +from pycord.utils import form_qs + +if TYPE_CHECKING: + from .state import State + +__all__ = ( + "Asset", +) + +AssetFormat = Literal["png", "jpg", "jpeg", "webp", "gif", "json"] + +VALID_STATIC_ASSET_FORMATS = ("png", "jpg", "jpeg", "webp", "json") +VALID_ASSET_FORMATS = VALID_STATIC_ASSET_FORMATS + ("gif",) + + +class Asset: + """ + Represents a Discord asset. + + Attributes + ---------- + url: :class:`str` + The URL of the asset. + animated: :class:`bool` + Whether or not the asset is animated. + """ + BASE_URL = "https://cdn.discordapp.com/" + + def __init__( + self, + state: State, + *, + url: str, + animated: bool = False, + ) -> None: + self._state: "State" = state + self.url = url + self.animated = animated + + def __str__(self) -> str: + return self.url + + def __repr__(self) -> str: + return f"" + + def url_with_parameters(self, *, format: AssetFormat | None = None, size: int | None = None) -> str: + """ + Returns the URL of the asset with the given parameters. + + Parameters + ---------- + format: :class:`str` + The file format of the asset. + Must be one of ``png``, ``jpg``, ``jpeg``, ``webp``, ``json``, or ``gif`` for animated assets. + size: :class:`int` + The size of the asset. Must be a power of 2 between 16 and 4096. + + Returns + ------- + :class:`str` + The asset's URL with the specified parameters. + + Raises + ------ + :exc:`ValueError` + The format or size is invalid. + """ + if not format and not size: + return self.url + if size: + # validate size, must be a power of 2 between 16 and 4096 + if size < 16 or size > 4096 or (size & (size - 1)): + raise ValueError("size must be a power of 2 between 16 and 4096") + if format: + if format not in VALID_ASSET_FORMATS: + raise ValueError(f"format must be one of {VALID_ASSET_FORMATS}") + if not self.animated and format not in VALID_STATIC_ASSET_FORMATS: + raise ValueError(f"format must be one of {VALID_STATIC_ASSET_FORMATS}") + url = urlparse(self.url) + if format: + url = url._replace(path=url.path + "." + format) + if size: + qs = parse_qs(url.query) + qs["size"] = [str(size)] + url = url._replace(query=form_qs("", **qs)) + url = url.geturl() + return url + + async def get(self, *, format: AssetFormat | None = None, size: int | None = None) -> bytes: + """ + Fetches the asset from Discord. + + Parameters + ---------- + format: :class:`str` + The file format of the asset. + Must be one of ``png``, ``jpg``, ``jpeg``, ``webp``, ``json``, or ``gif`` for animated assets. + size: :class:`int` + The size of the asset. Must be a power of 2 between 16 and 4096. + + Returns + ------- + :class:`bytes` + The asset data. + + Raises + ------ + :exc:`ValueError` + The format or size is invalid. + :exc:`HTTPException` + Fetching the asset failed. + """ + url = self.url_with_parameters(format=format, size=size) + return await self._state.http.get_from_cdn(url) + + @classmethod + def from_custom_emoji(cls, state: State, id: int, animated: bool = False) -> Self: + url = cls.BASE_URL + f"emojis/{id}.png" + return cls(state, url=url, animated=animated) + + @classmethod + def from_guild_icon(cls, state: State, guild_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"icons/{guild_id}/{hash}.png" + return cls(state, url=url, animated=hash.startswith("a_")) + + @classmethod + def from_guild_splash(cls, state: State, guild_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"splashes/{guild_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_guild_discovery_splash(cls, state: State, guild_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"discovery-splashes/{guild_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_guild_banner(cls, state: State, guild_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"banners/{guild_id}/{hash}.png" + return cls(state, url=url, animated=hash.startswith("a_")) + + @classmethod + def from_user_banner(cls, state: State, user_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"banners/{user_id}/{hash}.png" + return cls(state, url=url, animated=hash.startswith("a_")) + + @classmethod + def from_default_user_avatar(cls, state: State, index: int) -> Self: + url = cls.BASE_URL + f"embed/avatars/{index}.png" + return cls(state, url=url) + + @classmethod + def from_user_avatar(cls, state: State, user_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"avatars/{user_id}/{hash}.png" + return cls(state, url=url, animated=hash.startswith("a_")) + + @classmethod + def from_guild_member_avatar(cls, state: State, guild_id: int, user_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"guilds/{guild_id}/users/{user_id}/avatars/{hash}.png" + return cls(state, url=url, animated=hash.startswith("a_")) + + @classmethod + def from_user_avatar_decoration(cls, state: State, user_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"avatars/{user_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_application_icon(cls, state: State, application_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"app-icons/{application_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_application_cover(cls, state: State, application_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"app-icons/{application_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_application_asset(cls, state: State, application_id: int, asset_id: int) -> Self: + url = cls.BASE_URL + f"app-assets/{application_id}/{asset_id}.png" + return cls(state, url=url) + + @classmethod + def from_achievement_icon(cls, state: State, application_id: int, achievement_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"app-assets/{application_id}/achievements/{achievement_id}/icons/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_store_page_asset(cls, state: State, application_id: int, asset_id: int) -> Self: + url = cls.BASE_URL + f"app-assets/{application_id}/store/{asset_id}.png" + return cls(state, url=url) + + @classmethod + def from_sticker_pack_banner(cls, state: State, sticker_pack_banner_asset_id: int) -> Self: + url = cls.BASE_URL + f"app-assets/710982414301790216/store/{sticker_pack_banner_asset_id}.png" + return cls(state, url=url) + + @classmethod + def from_team_icon(cls, state: State, team_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"team-icons/{team_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_sticker(cls, state: State, sticker_id: int, format: AssetFormat) -> Self: + url = cls.BASE_URL + f"stickers/{sticker_id}.{format}" + return cls(state, url=url) + + @classmethod + def from_role_icon(cls, state: State, role_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"role-icons/{role_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_guild_scheduled_event_cover(cls, state: State, guild_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"guild-events/{guild_id}/{hash}.png" + return cls(state, url=url) + + @classmethod + def from_guild_member_banner(cls, state: State, guild_id: int, user_id: int, hash: str) -> Self: + url = cls.BASE_URL + f"banners/{guild_id}/{user_id}/{hash}.png" + return cls(state, url=url) diff --git a/pycord/audit_log.py b/pycord/audit_log.py index dcba3646..d49cafca 100644 --- a/pycord/audit_log.py +++ b/pycord/audit_log.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -19,195 +19,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from .auto_moderation import AutoModRule -from .channel import Thread -from .enums import AuditLogEvent -from .integration import Integration -from .missing import MISSING, Maybe, MissingEnum -from .scheduled_event import ScheduledEvent -from .snowflake import Snowflake -from .types import ( - ApplicationCommand as DiscordApplicationCommand, - AuditLog as DiscordAuditLog, - AuditLogChange as DiscordAuditLogChange, - AuditLogEntry as DiscordAuditLogEntry, - OptionalAuditEntryInfo as DiscordOptionalAuditEntryInfo, -) -from .user import User -from .webhook import GuildWebhook - -if TYPE_CHECKING: - from .state import State - - -class OptionalAuditEntryInfo: - """ - Represents Optionalized Audit Log Information. - - Attributes - ---------- - application_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - auto_moderation_rule_name: :class:`str` | :class:`.undefined.MissingEnum` - auto_moderation_rule_trigger_type: :class:`str` | :class:`.undefined.MissingEnum` - channel_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - count: :class:`int` | :class:`.undefined.MissingEnum` - delete_member_days: :class:`int` | :class:`.undefined.MissingEnum` - id: :class:`Snowflake` | :class:`.undefined.MissingEnum` - members_removed: :class:`int` | :class:`.undefined.MissingEnum` - message_id: :class:`Snowflake` | :class:`.undefined.MissingEnum` - role_name: :class:`str` | :class:`.undefined.MissingEnum` - type: :class:`int` | :class:`.undefined.MissingEnum` - """ - - __slots__ = ( - 'application_id', - 'auto_moderation_rule_name', - 'auto_moderation_rule_trigger_type', - 'channel_id', - 'count', - 'delete_member_days', - 'id', - 'members_removed', - 'message_id', - 'role_name', - 'type', - ) - - def __init__(self, data: DiscordOptionalAuditEntryInfo) -> None: - self.application_id: Snowflake | MissingEnum = ( - Snowflake(data['application_id']) if 'application_id' in data else MISSING - ) - self.auto_moderation_rule_name: str | MissingEnum = data.get( - 'auto_moderation_rule_name', MISSING - ) - self.auto_moderation_rule_trigger_type: str | MissingEnum = data.get( - 'auto_moderation_rule_trigger_type', MISSING - ) - self.channel_id: Snowflake | MissingEnum = ( - Snowflake(data['channel_id']) if 'channel_id' in data else MISSING - ) - self.count: int | MissingEnum = ( - int(data['count']) if 'count' in data else MISSING - ) - self.delete_member_days: int | MissingEnum = ( - int(data['delete_member_days']) if 'delete_member_days' in data else MISSING - ) - self.id: Snowflake | MissingEnum = ( - Snowflake(data['id']) if 'id' in data else MISSING - ) - self.members_removed: int | MissingEnum = ( - int(data['members_removed']) if 'members_removed' in data else MISSING - ) - self.message_id: Snowflake | MissingEnum = ( - Snowflake(data['message_id']) if 'message_id' in data else MISSING - ) - self.role_name: str | MissingEnum = data.get('role_name', MISSING) - self.type: int | MissingEnum = int(data['type']) if 'type' in data else MISSING - - -class AuditLogChange: - """ - Represents an Audit Log Change - - Attributes - ---------- - key: :class:`str` - new_value: :class:`typing.Any` | :class:`.undefined.MissingEnum` - old_value: :class:`typing.Any` | :class:`.undefined.MissingEnum` - """ - - __slots__ = ('key', 'new_value', 'old_value') - - def __init__(self, data: DiscordAuditLogChange) -> None: - self.key: str = data['key'] - self.new_value: Any | MissingEnum = data.get('new_value', MISSING) - self.old_value: Any | MissingEnum = data.get('old_value', MISSING) - - -class AuditLogEntry: - """ - Represents an entry into the Audit Log - - Attributes - ---------- - id: :class:`.snowflake.Snowflake` - target_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - user_id: :class:`.snowflake.Snowflake` | :class:`.undefined.MissingEnum` - action_type: :class:`.audit_log.AuditLogEvent` - options: :class:`.audit_log.OptionalAuditEntryInfo` | :class:`.undefined.MissingEnum` - reason: :class:`str` | :class:`.undefined.MissingEnum` - """ - - __slots__ = ( - '_changes', - 'id', - 'target_id', - 'user_id', - 'action_type', - 'options', - 'reason', - ) - - def __init__(self, data: DiscordAuditLogEntry) -> None: - self.target_id: Snowflake | MissingEnum = ( - Snowflake(data['target_id']) if data['target_id'] is not None else MISSING - ) - self._changes: list[AuditLogChange] = [ - AuditLogChange(change) for change in data.get('changes', []) - ] - self.user_id: Snowflake | MissingEnum = ( - Snowflake(data['user_id']) if data['user_id'] is not None else MISSING - ) - - self.id: Snowflake = Snowflake(data['id']) - self.action_type: AuditLogEvent = AuditLogEvent(data['action_type']) - self.options: OptionalAuditEntryInfo | MissingEnum = ( - OptionalAuditEntryInfo(data['options']) - if data.get('options') is not None - else MISSING - ) - self.reason: str | MissingEnum = data.get('reason', MISSING) - - -class AuditLog: - """ - Represents an Audit Log inside a Guild - - Attributes - ---------- - audit_log_entries: list[:class:`.audit_log.AuditLogEntry`] - auto_moderation_rules: list[:class:`.auto_moderation.AutoModRule`] - guild_scheduled_events: list[:class:`.scheduled_event.ScheduledEvent`] - integrations: list[:class:`.user.Integration`] - users: list[:class:`.user.User`] - webhooks: list[:class:`.webhook.GuildWebhook`] - """ - - def __init__(self, data: DiscordAuditLog, state: State) -> None: - # TODO: use models for these - self._application_commands: list[DiscordApplicationCommand] = data[ - 'application_commands' - ] - self.audit_log_entries: list[AuditLogEntry] = [ - AuditLogEntry(entry) for entry in data['audit_log_entries'] - ] - self.auto_moderation_rules: list[AutoModRule] = [ - AutoModRule(rule, state) for rule in data['auto_moderation_rules'] - ] - self.guild_scheduled_events: list[ScheduledEvent] = [ - ScheduledEvent(event, state) for event in data['guild_scheduled_events'] - ] - self.integrations: list[Integration] = [ - Integration(i) for i in data.get('integrations', []) - ] - self.threads: list[Thread] = [ - Thread(thread, state) for thread in data['threads'] - ] - self.users: list[User] = [User(user, state) for user in data['users']] - self.webhooks: list[GuildWebhook] = [ - GuildWebhook(w, state) for w in data['webhooks'] - ] +# TODO: Add audit log diff --git a/pycord/auto_moderation.py b/pycord/auto_moderation.py index acbff8ec..dbd94824 100644 --- a/pycord/auto_moderation.py +++ b/pycord/auto_moderation.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -19,142 +19,452 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE +# ah! we've been cursed! +# from curses import meta + from __future__ import annotations -from typing import TYPE_CHECKING +import datetime -from .enums import ( - AutoModActionType, - AutoModEventType, - AutoModKeywordPresetType, - AutoModTriggerType, -) -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import ( - AutoModerationAction as DiscordAutoModerationAction, - AutoModerationActionMetadata as DiscordAutoModerationActionMetadata, - AutoModerationRule as DiscordAutoModerationRule, - AutoModerationTriggerMetadata as DiscordAutoModerationTriggerMetadata, -) +from .enums import AutoModEventType, AutoModTriggerType, AutoModActionType, AutoModKeywordPresetType +from .mixins import Identifiable +from .missing import Maybe, MISSING + +from typing import Any, Type, TYPE_CHECKING, Self, Union if TYPE_CHECKING: + from discord_typings import ( + AutoModerationRuleData, + KeywordAutoModerationTriggerMetadata as KeywordAutoModerationTriggerMetadataData, + AutoModerationTriggerMetadataData, + KeywordPresetAutoModerationTriggerMetadataData, + MentionSpamAutoModerationTriggerMetadataData, + AutoModerationActionMetadataData, + TimeoutAutoModerationActionMetadataData, + BlockmessageAutoModerationActionMetadataData, + SendAlertMessageAutoModerationActionMetadataData, + AutoModerationActionData as AutoModerationActionData, + ) + from .guild import Guild + from .mixins import Snowflake from .state import State +__all__ = ( + "AutoModRule", + "AutoModTriggerMetadata", + "KeywordAutoModTriggerMetadata", + "KeywordPresetAutoModTriggerMetadata", + "MentionSpamAutoModTriggerMetadata", + "AutoModAction", + "AutoModActionMetadata", + "TimeoutAutoModActionMetadata", + "BlockMessageAutoModActionMetadata", + "SendAlertMessageAutoModActionMetadata", +) -class AutoModTriggerMetadata: - def __init__(self, data: DiscordAutoModerationTriggerMetadata) -> None: - self.keyword_filter: list[str] | MissingEnum = data.get( - 'keyword_filter', MISSING - ) - self.regex_patterns: list[str] | MissingEnum = data.get( - 'regex_patterns', MISSING - ) - self.presets: list[AutoModKeywordPresetType] | MissingEnum = ( - [AutoModKeywordPresetType(preset) for preset in data['presets']] - if data.get('presets') is not None - else MISSING - ) - self.allow_list: list[str] | MissingEnum = data.get('allow_list', MISSING) - self.mention_total_limit: int | MissingEnum = data.get( - 'mention_total_limit', MISSING - ) +class AutoModRule(Identifiable): + """ + Represents an auto moderation rule. -class AutoModActionMetadata: - def __init__(self, data: DiscordAutoModerationActionMetadata) -> None: - self.channel_id: Snowflake | MissingEnum = ( - Snowflake(data['channel_id']) - if data.get('channel_id') is not None - else MISSING - ) - self.duration_seconds: int | MissingEnum = data.get('duration_seconds', MISSING) + Attributes + ---------- + id: :class:`int` + The rule's ID. + guild_id: :class:`int` + The ID of the guild this rule is in. + name: :class:`str` + The rule's name. + creator_id: :class:`int` + The ID of the user who created this rule. + event_type: :class:`AutoModEventType` + The type of event this rule is for. + trigger_type: :class:`AutoModTriggerType` + The trigger type used by this rule. + trigger_metadata: :class:`AutoModTriggerMetadata` + The metadata for the trigger. Type corresponds to the trigger type. + actions: list[:class:`AutoModAction`] + The actions to take when this rule is triggered. + enabled: :class:`bool` + Whether or not this rule is enabled. + exempt_role_ids: list[:class:`int`] + The roles that are exempt from this rule. + exempt_channel_ids: list[:class:`int`] + The channels that are exempt from this rule. + """ + __slots__ = ( + "_state", + "id", + "guild_id", + "name", + "creator_id", + "event_type", + "trigger_type", + "trigger_metadata", + "actions", + "enabled", + "exempt_role_ids", + "exempt_channel_ids", + ) + + def __init__(self, data: AutoModerationRuleData, state: State) -> None: + self._state: State = state + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + self.name: str = data["name"] + self.creator_id: int = int(data["creator_id"]) + self.event_type: AutoModEventType = AutoModEventType(data["event_type"]) + self.trigger_type: AutoModTriggerType = AutoModTriggerType(data["trigger_type"]) + self.trigger_metadata: Maybe[AutoModTriggerMetadata] = trigger_metadata_factory(data) + self.actions: list[AutoModAction] = [AutoModAction.from_data(x) for x in data["actions"]] + self.enabled: bool = data["enabled"] + self.exempt_role_ids: list[int] = [int(x) for x in data["exempt_roles"]] + self.exempt_channel_ids: list[int] = [int(x) for x in data["exempt_channels"]] + def __repr__(self) -> str: + return f"" -class AutoModAction: - def __init__(self, data: DiscordAutoModerationAction) -> None: - self.type: AutoModActionType = AutoModActionType(data['type']) - self.metadata: AutoModActionMetadata = AutoModActionMetadata(data['metadata']) + def __str__(self) -> str: + return self.name + @property + def guild(self) -> Guild: + # TODO: fetch from cache + raise NotImplementedError -class AutoModRule: - def __init__(self, data: DiscordAutoModerationRule, state: State) -> None: - self._state: State = state - self.id: Snowflake = Snowflake(data['id']) - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self.name: str = data['name'] - self.creator_id: Snowflake = Snowflake(data['creator_id']) - self.event_type: AutoModEventType = AutoModEventType(data['event_type']) - self.trigger_type: AutoModTriggerType = AutoModTriggerType(data['trigger_type']) - self.trigger_metadata: AutoModTriggerMetadata = AutoModTriggerMetadata( - data['trigger_metadata'] - ) - self.actions: list[AutoModAction] = [ - AutoModAction(action) for action in data['actions'] - ] - self.enabled: bool = data['enabled'] - self.exempt_roles: list[Snowflake] = [ - Snowflake(role) for role in data.get('exempt_roles', []) - ] - self.exempt_channels: list[Snowflake] = [ - Snowflake(member) for member in data.get('exempt_channels', []) - ] - - async def edit( + async def modify( self, *, - name: str | MissingEnum = MISSING, - event_type: AutoModEventType | MissingEnum = MISSING, - trigger_metadata: AutoModTriggerMetadata | MissingEnum = MISSING, - actions: list[AutoModAction] | MissingEnum = MISSING, - enabled: bool | MissingEnum = MISSING, - exempt_roles: list[Snowflake] | MissingEnum = MISSING, - exempt_channels: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, + name: Maybe[str] = MISSING, + trigger_type: Maybe[AutoModTriggerType] = MISSING, + trigger_metadata: Maybe[AutoModTriggerMetadata] = MISSING, + actions: Maybe[list[AutoModAction]] = MISSING, + enabled: Maybe[bool] = MISSING, + exempt_roles: Maybe[list[Snowflake]] = MISSING, + exempt_channels: Maybe[list[Snowflake]] = MISSING, + reason: Maybe[str] = MISSING, ) -> AutoModRule: - """Edits the auto moderation rule. + """ + Modifies this rule. Parameters ---------- name: :class:`str` - The rule name. - event_type: :class:`AutoModEventType` - The event type. + The new name for this rule. + trigger_type: :class:`AutoModTriggerType` + The new trigger type for this rule. trigger_metadata: :class:`AutoModTriggerMetadata` - The trigger metadata. + The new trigger metadata for this rule. actions: list[:class:`AutoModAction`] - The actions to take when the rule is triggered. + The new actions for this rule. enabled: :class:`bool` - Whether the rule is enabled. - exempt_roles: list[:class:`Snowflake`] - The roles exempt from the rule. - exempt_channels: list[:class:`Snowflake`] - The channels exempt from the rule. - reason: :class:`str` | None - The reason for editing this rule. Appears in the guild's audit log. + Whether or not this rule is enabled. + exempt_roles: list[:class:`.Snowflake`] + The roles that are exempt from this rule. + exempt_channels: list[:class:`.Snowflake`] + The channels that are exempt from this rule. + reason: :class:`str` + The reason for modifying this rule. Shows up in the audit log. + + Returns + ------- + :class:`AutoModRule` + The modified rule. + + Raises + ------ + :exc:`.Forbidden` + You do not have permission to modify this rule. + :exc:`.NotFound` + The rule was not found, it may have been deleted. + :exc:`.HTTPException` + Modifying the rule failed. """ - data = await self._state.http.modify_auto_moderation_rule( - self.guild_id, - self.id, - name=name, - event_type=event_type, - trigger_metadata=trigger_metadata, - actions=actions, - enabled=enabled, - exempt_roles=exempt_roles, - exempt_channels=exempt_channels, - reason=reason, - ) - return AutoModRule(data, self._state) + payload = { + "name": name, + "trigger_type": trigger_type, + "trigger_metadata": trigger_metadata, + "actions": actions, + "enabled": enabled, + "exempt_roles": [x.id for x in exempt_roles] if exempt_roles else MISSING, + "exempt_channels": [x.id for x in exempt_channels] if exempt_channels else MISSING, + } + return await self._state.http.modify_auto_moderation_rule(self.guild_id, self.id, **payload, reason=reason) - async def delete(self, *, reason: str | None = None) -> None: - """Deletes the auto moderation rule. + async def delete(self, *, reason: Maybe[str]) -> None: + """ + Deletes this rule. Parameters ---------- - reason: :class:`str` | None - The reason for deleting this rule. Appears in the guild's audit log. + reason: :class:`str` + The reason for deleting this rule. Shows up in the audit log. + + Raises + ------ + :exc:`.Forbidden` + You do not have permission to delete this rule. + :exc:`.NotFound` + The rule was not found, it may have already been deleted. + :exc:`.HTTPException` + Deleting the rule failed. """ - await self._state.http.delete_auto_moderation_rule( - self.guild_id, self.id, reason=reason + return await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id, reason=reason) + + +class _BaseTriggerMetadata: + @classmethod + def from_data(cls: Type[AutoModTriggerMetadata], data: AutoModerationTriggerMetadataData) -> AutoModTriggerMetadata: + return cls(**data) + + def to_data(self) -> AutoModerationTriggerMetadataData: + raise NotImplementedError + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {''.join([f'{k}={v!r}' for k, v in self.__dict__.items()])}>" + + +class KeywordAutoModTriggerMetadata(_BaseTriggerMetadata): + """ + Represents the metadata for a trigger of type :attr:`.AutoModTriggerType.KEYWORD`. + + Attributes + ---------- + keyword_filter: list[:class:`str`] + The keywords to filter. + regex_patterns: list[:class:`str`] + The regex patterns to filter. + allow_list: list[:class:`str`] + The list of words to allow. + """ + __slots__ = ("keyword_filter", "regex_patterns", "allow_list") + + def __init__( + self, + *, + keyword_filter: list[str], + regex_patterns: list[str], + allow_list: list[str], + ) -> None: + self.keyword_filter: list[str] = keyword_filter + self.regex_patterns: list[str] = regex_patterns + self.allow_list: list[str] = allow_list + + def to_data(self) -> KeywordAutoModerationTriggerMetadataData: + return { + "keyword_filter": self.keyword_filter, + "regex_patterns": self.regex_patterns, + "allow_list": self.allow_list, + } + + +class KeywordPresetAutoModTriggerMetadata(_BaseTriggerMetadata): + """ + Represents the metadata for a trigger of type :attr:`.AutoModTriggerType.KEYWORD_PRESET`. + + Attributes + ---------- + presets: list[:class:`.AutoModKeywordPresetType`] + The presets to use. + allow_list: list[:class:`str`] + The list of words to allow. + """ + __slots__ = ("presets", "allow_list") + + def __init__( + self, + *, + presets: list[AutoModKeywordPresetType], + allow_list: list[str], + ) -> None: + self.presets: list[AutoModKeywordPresetType] = presets + self.allow_list: list[str] = allow_list + + def to_data(self) -> KeywordPresetAutoModerationTriggerMetadataData: + return { + "presets": [x.value for x in self.presets], + "allow_list": self.allow_list, + } + + +class MentionSpamAutoModTriggerMetadata(_BaseTriggerMetadata): + """ + Represents the metadata for a trigger of type :attr:`.AutoModTriggerType.MENTION_SPAM`. + + Attributes + ---------- + mention_total_limit: :class:`int` + The number of mentions that can be sent before triggering the rule. + mention_raid_protection_enabled: :class:`bool` + Whether or not raid protection is enabled. + """ + __slots__ = ("mention_total_limit", "mention_raid_protection_enabled") + + def __init__( + self, + *, + mention_total_limit: int, + mention_raid_protection_enabled: bool, + ) -> None: + self.mention_total_limit: int = mention_total_limit + self.mention_raid_protection_enabled: bool = mention_raid_protection_enabled + + def to_data(self) -> "MentionSpamAutoModerationTriggerMetadataData": + return { + "mention_total_limit": self.mention_total_limit, + "mention_raid_protection_enabled": self.mention_raid_protection_enabled, + } + + +AutoModTriggerMetadata = Union[ + KeywordAutoModTriggerMetadata, + KeywordPresetAutoModTriggerMetadata, + MentionSpamAutoModTriggerMetadata, +] + + +def trigger_metadata_factory(data: "AutoModerationRuleData") -> AutoModTriggerMetadata: + trigger_type = AutoModTriggerType(data["trigger_type"]) + return { + AutoModTriggerType.KEYWORD: KeywordAutoModTriggerMetadata, + AutoModTriggerType.KEYWORD_PRESET: KeywordPresetAutoModTriggerMetadata, + AutoModTriggerType.MENTION_SPAM: MentionSpamAutoModTriggerMetadata, + }[trigger_type].from_data(data["trigger_metadata"]) + + +class AutoModAction: + """ + Represents an auto moderation action. + + Attributes + ---------- + type: :class:`AutoModActionType` + The type of action. + metadata: :class:`AutoModActionMetadata` + The metadata for the action. + """ + + def __init__( + self, + *, + type: AutoModActionType, + metadata: Maybe["AutoModActionMetadata"] = MISSING, + ) -> None: + self.type: AutoModActionType = type + self.metadata: Maybe["AutoModActionMetadata"] = metadata + + @classmethod + def from_data(cls, data: "AutoModerationActionData") -> Self: + _type = AutoModActionType(data["type"]) + return cls( + type=_type, + metadata=action_metadata_factory(data) if "metadata" in data else MISSING, ) + + def to_data(self) -> "AutoModerationActionData": + payload = { + "type": self.type.value, + } + if self.metadata is not MISSING: + payload["metadata"] = self.metadata.to_data() # type: ignore + return payload # type: ignore + + +class _BaseActionMetadata: + @classmethod + def from_data(cls, data: "AutoModerationActionMetadataData") -> Self: + return cls(**data) + + def to_data(self) -> "AutoModerationActionMetadataData": + raise NotImplementedError + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {''.join([f'{k}={v!r}' for k, v in self.__dict__.items()])}>" + + +class TimeoutAutoModActionMetadata(_BaseActionMetadata): + """ + Represents the metadata for an action of type :attr:`.AutoModActionType.TIMEOUT`. + + Attributes + ---------- + duration: :class:`datetime.timedelta` + The duration of the timeout. + """ + __slots__ = ("duration",) + + def __init__( + self, + *, + duration: datetime.timedelta, + ) -> None: + self.duration: datetime.timedelta = duration + + def to_data(self) -> "TimeoutAutoModerationActionMetadataData": + return { + "duration_seconds": int(self.duration.total_seconds()), + } + + +class BlockMessageAutoModActionMetadata(_BaseActionMetadata): + """ + Represents the metadata for an action of type :attr:`.AutoModActionType.BLOCK_MESSAGE`. + + Attributes + ---------- + custom_message: :class:`str` + The custom message to show to the user when their message is blocked. + """ + __slots__ = ("custom_message",) + + def __init__( + self, + *, + custom_message: str, + ) -> None: + self.custom_message: str = custom_message + + def to_data(self) -> "BlockmessageAutoModerationActionMetadataData": + return { + "custom_message": self.custom_message, + } + + +class SendAlertMessageAutoModActionMetadata(_BaseActionMetadata): + """ + Represents the metadata for an action of type :attr:`.AutoModActionType.SEND_ALERT_MESSAGE`. + + Attributes + ---------- + channel_id: :class:`int` + The ID of the channel to send the alert message to. + """ + __slots__ = ("channel_id",) + + def __init__( + self, + *, + channel_id: int, + ) -> None: + self.channel_id: int = channel_id + + def to_data(self) -> "SendAlertMessageAutoModerationActionMetadataData": + return { + "channel_id": self.channel_id, + } + + +AutoModActionMetadata = Union[ + TimeoutAutoModActionMetadata, + BlockMessageAutoModActionMetadata, + SendAlertMessageAutoModActionMetadata, +] + + +def action_metadata_factory(data: "AutoModerationActionData") -> AutoModActionMetadata: + action_type = AutoModActionType(data["type"]) + return { + AutoModActionType.TIMEOUT: TimeoutAutoModActionMetadata, + AutoModActionType.BLOCK_MESSAGE: BlockMessageAutoModActionMetadata, + AutoModActionType.SEND_ALERT_MESSAGE: SendAlertMessageAutoModActionMetadata, + }[action_type].from_data(data["metadata"]) diff --git a/pycord/bot.py b/pycord/bot.py index 7a49a100..e71abc80 100644 --- a/pycord/bot.py +++ b/pycord/bot.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,555 +18,57 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE + + import asyncio -from typing import Any, AsyncGenerator, Type, TypeVar +from typing import Any, Iterable from aiohttp import BasicAuth -from .application_role_connection_metadata import ApplicationRoleConnectionMetadata -from .audit_log import AuditLog -from .commands import Group -from .commands.application.command import ApplicationCommand -from .enums import ( - DefaultMessageNotificationLevel, - ExplicitContentFilterLevel, - VerificationLevel, -) -from .errors import BotException, NoIdentifiesLeft, OverfilledShardsException -from .events.event_manager import Event -from .file import File -from .flags import Intents, SystemChannelFlags -from .gateway import PassThrough, ShardCluster, ShardManager -from .guild import Guild, GuildPreview from .interface import print_banner, start_logging -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .state import State -from .types import AsyncFunc -from .types.audit_log import AUDIT_LOG_EVENT_TYPE -from .user import User -from .utils import chunk, get_arg_defaults - -T = TypeVar('T') +from .state.cache import Store +from .state.core import BASE_MODELS, State class Bot: - """ - The class for interacting with Discord with a Bot. - - Parameters - ---------- - intents: :class:`.flags.Intents` - The Gateway Intents to use - print_banner_on_startup - Whether to print the banner on startup or not - logging_flavor: Union[int, str, dict[str, :class:`typing.Any`], None] - The logging flavor this bot uses - - Defaults to `None`. - max_messages: :class:`int` - The maximum amount of Messages to cache - shards: :class:`int` | list[:class:`int`] - The amount of shards this bot should launch with. - - Defaults to 1. - proxy: :class:`str` | None - The proxy to use - proxy_auth: :class:`aiohttp.BasicAuth` | None - The authentication of your proxy. - - Defaults to `None`. - global_shard_status: :class:`int` - The amount of shards globally deployed. - Only supported on bots not using `.cluster`. - - Attributes - ---------- - user: :class:`.user.User` - The user within this bot - guilds: list[:class:`.guild.Guild`] - The Guilds this Bot is in - """ - def __init__( self, - intents: Intents, - print_banner_on_startup: bool = True, - logging_flavor: int | str | dict[str, Any] | None = None, - max_messages: int = 1000, - shards: int | list[int] | None = None, - global_shard_status: int | None = None, + token: str, + intents: int, + shards: Iterable[int] | None = None, + max_message_cache: int = 10000, + max_member_cache: int = 50000, + base_url: str = "https://discord.com/api/v10", proxy: str | None = None, proxy_auth: BasicAuth | None = None, - verbose: bool = False, + store_class: type[Store] = Store, + model_classes: dict[Any, Any] = BASE_MODELS, ) -> None: - self.intents: Intents = intents - self.max_messages: int = max_messages - self._state: State = State( - intents=self.intents, max_messages=self.max_messages, verbose=verbose - ) - self._shards = shards - self._logging_flavor: int | str | dict[str, Any] = logging_flavor - self._print_banner = print_banner_on_startup - self._proxy = proxy - self._proxy_auth = proxy_auth - if shards and not global_shard_status: - if isinstance(shards, list): - self._global_shard_status = len(shards) - else: - self._global_shard_status = int(shards) - elif global_shard_status: - self._global_shard_status = global_shard_status - else: - self._global_shard_status = None - - @property - def user(self) -> User: - return self._state.user - - async def _run_async(self, token: str) -> None: - start_logging(flavor=self._logging_flavor) - self._state.bot_init( - token=token, clustered=False, proxy=self._proxy, proxy_auth=self._proxy_auth + self.state = State( + token=token, + max_messages=max_message_cache, + max_members=max_member_cache, + intents=intents, + base_url=base_url, + proxy=proxy, + proxy_auth=proxy_auth, + store_class=store_class, + cache_model_classes=model_classes, + shards=shards, ) - info = await self._state.http.get_gateway_bot() - session_start_limit = info['session_start_limit'] - - self._state.shard_concurrency = PassThrough( - session_start_limit['max_concurrency'], 7 - ) - self._state._session_start_limit = session_start_limit - - if self._shards is None: - shards = list(range(info['shards'])) - else: - shards: list[int] = ( - self._shards - if isinstance(self._shards, list) - else list(range(self._shards)) - ) - - if session_start_limit['remaining'] == 0: - raise NoIdentifiesLeft('session_start_limit has been exhausted') - elif session_start_limit['remaining'] - len(shards) <= 0: - raise NoIdentifiesLeft('session_start_limit will be exhausted') - - sharder = ShardManager( - self._state, - shards, - self._global_shard_status or len(shards), - proxy=self._proxy, - proxy_auth=self._proxy_auth, - ) - await sharder.start() - self._state.shard_managers.append(sharder) - while not self._state.raw_user: - self._state._raw_user_fut: asyncio.Future[None] = asyncio.Future() - await self._state._raw_user_fut - - if self._print_banner: - printable_shards = 0 - - if self._shards is None: - printable_shards = len(shards) - else: - printable_shards = ( - self._shards if isinstance(self._shards, int) else len(self._shards) - ) - - print_banner( - self._state._session_start_limit['remaining'], - printable_shards, - bot_name=self.user.name, - ) + async def start( + self, logging_flavor: None | int | str | dict[str, Any] = None + ) -> None: + await self.state.http.force_start() + gateway_data = await self.state.http.get_gateway_bot() + await self.state.gateway.start(gateway_data) - await self._run_until_exited() + start_logging(flavor=logging_flavor) + print_banner(10, len(self.state.shards), "") - async def _run_until_exited(self) -> None: try: await asyncio.Future() except (asyncio.CancelledError, KeyboardInterrupt): - # most things are already handled by the asyncio.run function - # the only thing we have to worry about are aiohttp errors - await self._state.http.close_session() - for sm in self._state.shard_managers: - await sm.session.close() - - if self._state._clustered: - for sc in self._state.shard_clusters: - sc.keep_alive.set_result(None) - - def run(self, token: str) -> None: - """ - Run the Bot without being clustered. - - .. WARNING:: - This blocks permanently and doesn't allow functions after it to run - - Parameters - ---------- - token: :class:`str` - The authentication token of this Bot. - """ - asyncio.run(self._run_async(token=token)) - - async def _run_cluster( - self, token: str, clusters: int, amount: int, managers: int - ) -> None: - start_logging(flavor=self._logging_flavor) - self._state.bot_init( - token=token, clustered=True, proxy=self._proxy, proxy_auth=self._proxy_auth - ) - - info = await self._state.http.get_gateway_bot() - session_start_limit = info['session_start_limit'] - - if self._shards is None: - shards = list(range(info['shards'])) - else: - shards = ( - self._shards - if isinstance(self._shards, list) - else list(range(self._shards)) - ) - - if session_start_limit['remaining'] == 0: - raise NoIdentifiesLeft('session_start_limit has been exhausted') - elif session_start_limit['remaining'] - len(shards) <= 0: - raise NoIdentifiesLeft('session_start_limit will be exhausted') - - self._state.shard_concurrency = PassThrough( - session_start_limit['max_concurrency'], 7 - ) - self._state._session_start_limit = session_start_limit - - sorts = list(chunk(shards, clusters)) - - for cluster in sorts: - cluster_class = ShardCluster( - self._state, - cluster, - amount, - managers, - proxy=self._proxy, - proxy_auth=self._proxy_auth, - ) - cluster_class.run() - self._state.shard_clusters.append(cluster_class) - - while not self._state.raw_user: - self._state._raw_user_fut: asyncio.Future[None] = asyncio.Future() - await self._state._raw_user_fut - - if self._print_banner: - print_banner( - concurrency=self._state._session_start_limit['remaining'], - shard_count=self._shards - if isinstance(self._shards, int) - else len(self._shards), - bot_name=self._state.user.name, - ) - - await self._run_until_exited() - - def cluster( - self, - token: str, - clusters: int, - amount: int | None = None, - managers: int | None = None, - ) -> None: - """ - Run the Bot in a clustered formation. - Much more complex but much more scalable. - - .. WARNING:: Shouldn't be used on Bots under ~300k Guilds - - Parameters - ---------- - token: :class:`str` - The authentication token of this Bot. - clusters: :class:`int` - The amount of clusters to run. - amount: :class:`int` - The full amount of shards that are/will be running globally (not just on this instance.) - managers: :class:`int` | :class:`int` - The amount of managers to hold per cluster. - - Defaults to `None` which automatically determines the amount. - """ - shards = self._shards if isinstance(self._shards, int) else len(self._shards) - - if clusters > shards: - raise OverfilledShardsException('Cannot have more clusters than shards') - - if not amount: - amount = shards - - if not managers: - managers = 1 - - if amount < shards: - raise OverfilledShardsException( - 'Cannot have a higher shard count than shard amount' - ) - - if managers > shards: - raise OverfilledShardsException('Cannot have more managers than shards') - - asyncio.run( - self._run_cluster( - token=token, clusters=clusters, amount=amount, managers=managers - ) - ) - - def listen(self, event: Event | None = None) -> T: - """ - Listen to an event - - Parameters - ---------- - event: :class:`Event` | None - The event to listen to. - Optional if using type hints. - """ - - def wrapper(func: T) -> T: - if event: - self._state.event_manager.add_event(event, func) - else: - args = get_arg_defaults(func) - - values = list(args.values()) - - if len(values) != 1: - raise BotException( - 'Only one argument is allowed on event functions' - ) - - eve = values[0] - - if eve[1] is None: - raise BotException( - 'Event must either be typed, or be present in the `event` parameter' - ) - - if not isinstance(eve[1](), Event): - raise BotException('Events must be of type Event') - - self._state.event_manager.add_event(eve[1], func) - - return func - - return wrapper - - def wait_for(self, event: T) -> asyncio.Future[T]: - return self._state.event_manager.wait_for(event) - - def command( - self, - name: str | MissingEnum = MISSING, - cls: T = ApplicationCommand, - **kwargs: Any, - ) -> T: - """ - Create a command within the Bot - - Parameters - ---------- - name: :class:`str` - The name of the Command. - cls: type of :class:`.commands.Command` - The command type to instantiate. - kwargs: dict[str, Any] - The kwargs to entail onto the instantiated command. - """ - - def wrapper(func: AsyncFunc) -> T: - command = cls(func, name=name, state=self._state, **kwargs) - self._state.commands.append(command) - return command - - return wrapper - - def group(self, name: str, cls: Type[Group], **kwargs: Any) -> T: - """ - Create a brand-new Group of Commands - - Parameters - ---------- - name: :class:`str` - The name of the Group. - cls: type of :class:`.commands.Group` - The group type to instantiate. - kwargs: dict[str, Any] - The kwargs to entail onto the instantiated group. - """ - - def wrapper(func: T) -> T: - return cls(func, name, state=self._state, **kwargs) - - return wrapper - - @property - async def guilds(self) -> AsyncGenerator[Guild, None]: - return await (self._state.store.sift('guilds')).get_all() - - async def get_application_role_connection_metadata_records( - self, - ) -> list[ApplicationRoleConnectionMetadata]: - """Get the application role connection metadata records. - - Returns - ------- - list[:class:`ApplicationRoleConnectionMetadata`] - The application role connection metadata records. - """ - data = await self._state.http.get_application_role_connection_metadata_records( - self.user.id - ) - return [ApplicationRoleConnectionMetadata.from_dict(record) for record in data] - - async def update_application_role_connection_metadata_records( - self, records: list[ApplicationRoleConnectionMetadata] - ) -> list[ApplicationRoleConnectionMetadata]: - """Update the application role connection metadata records. - - Parameters - ---------- - records: list[:class:`ApplicationRoleConnectionMetadata`] - The application role connection metadata records. - - Returns - ------- - list[:class:`ApplicationRoleConnectionMetadata`] - The updated application role connection metadata records. - """ - data = ( - await self._state.http.update_application_role_connection_metadata_records( - self.user.id, [record.to_dict() for record in records] - ) - ) - return [ApplicationRoleConnectionMetadata.from_dict(record) for record in data] - - async def create_guild( - self, - name: str, - *, - icon: File | MissingEnum = MISSING, - verification_level: VerificationLevel | MissingEnum = MISSING, - default_message_notifications: DefaultMessageNotificationLevel - | MissingEnum = MISSING, - explicit_content_filter: ExplicitContentFilterLevel | MissingEnum = MISSING, - roles: list[dict] | MissingEnum = MISSING, # TODO - channels: list[dict] | MissingEnum = MISSING, # TODO - afk_channel_id: Snowflake | MissingEnum = MISSING, - afk_timeout: int | MissingEnum = MISSING, - system_channel_id: Snowflake | MissingEnum = MISSING, - system_channel_flags: SystemChannelFlags | MissingEnum = MISSING, - ) -> Guild: - """Create a guild. - - Parameters - ---------- - name: :class:`str` - The name of the guild. - icon: :class:`.File` - The icon of the guild. - verification_level: :class:`VerificationLevel` - The verification level of the guild. - default_message_notifications: :class:`DefaultMessageNotificationLevel` - The default message notifications of the guild. - explicit_content_filter: :class:`ExplicitContentFilterLevel` - The explicit content filter level of the guild. - roles: list[dict] - The roles of the guild. - channels: list[dict] - The channels of the guild. - afk_channel_id: :class:`Snowflake` - The afk channel id of the guild. - afk_timeout: :class:`int` - The afk timeout of the guild. - system_channel_id: :class:`Snowflake` - The system channel id of the guild. - system_channel_flags: :class:`SystemChannelFlags` - The system channel flags of the guild. - - Returns - ------- - :class:`Guild` - The created guild. - """ - data = await self._state.http.create_guild( - name, - icon=icon, - verification_level=verification_level, - default_message_notifications=default_message_notifications, - explicit_content_filter=explicit_content_filter, - roles=roles, - channels=channels, - afk_channel_id=afk_channel_id, - afk_timeout=afk_timeout, - system_channel_id=system_channel_id, - system_channel_flags=system_channel_flags, - ) - return Guild(data, self._state) - - async def get_guild(self, guild_id: Snowflake) -> Guild: - """Get a guild. - - Parameters - ---------- - guild_id: :class:`Snowflake` - The guild id. - - Returns - ------- - :class:`Guild` - The guild. - """ - data = await self._state.http.get_guild(guild_id) - return Guild(data, self._state) - - async def get_guild_preview(self, guild_id: Snowflake) -> GuildPreview: - """Get a guild preview. - - Parameters - ---------- - guild_id: :class:`Snowflake` - The guild id. - - Returns - ------- - :class:`GuildPreview` - The guild preview. - """ - data = await self._state.http.get_guild_preview(guild_id) - return GuildPreview(data, self._state) - - async def fetch_guild_audit_log( - self, - guild_id: Snowflake, - user_id: Snowflake | MissingEnum = MISSING, - action_type: AUDIT_LOG_EVENT_TYPE | MissingEnum = MISSING, - before: Snowflake | MissingEnum = MISSING, - after: Snowflake | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> AuditLog: - """ - Fetches and returns the audit log. - - Returns - ------- - :class:`.AuditLog` - """ - raw_audit_log = await self._state.http.get_guild_audit_log( - guild_id=guild_id, - user_id=user_id, - action_type=action_type, - before=before, - after=after, - limit=limit, - ) - return AuditLog(raw_audit_log, self._state) + await self.state.http._session.close() + return diff --git a/pycord/channel.py b/pycord/channel.py index 2298951c..0710c202 100644 --- a/pycord/channel.py +++ b/pycord/channel.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,790 +18,1230 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE + from __future__ import annotations from datetime import datetime -from functools import cached_property -from typing import TYPE_CHECKING, Any +from discord_typings._resources._channel import FollowedChannelData + +from . import File +from .asset import Asset from .embed import Embed -from .enums import ChannelType, OverwriteType, VideoQualityMode -from .errors import ComponentException -from .file import File -from .flags import ChannelFlags, Permissions -from .member import Member -from .message import AllowedMentions, Message, MessageReference -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import ( - Channel as DiscordChannel, - DefaultReaction as DiscordDefaultReaction, - ForumTag as DiscordForumTag, - Overwrite as DiscordOverwrite, - ThreadMember as DiscordThreadMember, - ThreadMetadata as DiscordThreadMetadata, -) -from .typing import Typing +from .flags import ChannelFlags, MessageFlags, Permissions +from .enums import ChannelType, ForumLayoutType, OverwriteType, SortOrderType, VideoQualityMode +from .guild import GuildMember +from .message import AllowedMentions, Attachment, Message +from .missing import Maybe, MISSING +from .mixins import Identifiable, Messageable +from .object import Object +from .user import User + +from typing import cast, TYPE_CHECKING, Self, Union if TYPE_CHECKING: + from discord_typings import ( + ChannelData, + TextChannelData, + DMChannelData, + VoiceChannelData, + GroupDMChannelData, + CategoryChannelData, + NewsChannelData, + ThreadChannelData, + ForumChannelData, + MediaChannelData, + PermissionOverwriteData, + DefaultReactionData, + ForumTagData, + ThreadMemberData, + ThreadMetadataData, + PartialChannelData, + ) + + GuildChannelData = Union[ + TextChannelData, + VoiceChannelData, + CategoryChannelData, + NewsChannelData, + ForumChannelData, + MediaChannelData, + ThreadChannelData, + ] + + from .mixins import Snowflake from .state import State - from .ui.house import House +__all__ = ( + "Channel", + "TextChannel", + "DMChannel", + "VoiceChannel", + "GroupDMChannel", + "CategoryChannel", + "NewsChannel", + "Thread", + "StageChannel", + "ForumChannel", + "MediaChannel", + "channel_factory", +) -class _Overwrite: - __slots__ = ('id', 'type', 'allow', 'deny') - def __init__( - self, - id: int, - type: OverwriteType, - *, - allow: Permissions, - deny: Permissions, - ) -> None: - self.id: Snowflake = Snowflake(id) +class BaseChannel(Identifiable): + """ + The bare minimum for a Discord channel. All other channels inherit from this one. + + Attributes + ----------- + id: :class:`int` + The ID of the channel. + type: :class:`ChannelType` + The type of the channel. + flags: :class:`ChannelFlags` + The flags of the channel. + """ + __slots__ = ( + "_state", + "id", + "type", + "flags", + ) + + def __init__(self, data: ChannelData, state: State) -> None: + self._state: State = state + self._update(data) + + def _update(self, data: ChannelData) -> None: + self.id = int(cast(str, data["id"])) + self.type: ChannelType = ChannelType(data["type"]) + self.flags: ChannelFlags = ChannelFlags.from_value(flags) \ + if (flags := cast(int, data.get("flags"))) \ + else ChannelFlags(0) + + async def _modify(self, *, reason: str | None = None, **kwargs) -> Self: + data: ChannelData = await self._state.http.modify_channel(self.id, **kwargs, reason=reason) + return self.__class__(data, self._state) + + async def delete(self, *, reason: str | None = None) -> None: + return await self._state.http.delete_channel(self.id, reason=reason) + + +class GuildChannel(BaseChannel): + # For guilds + __slots__ = ( + "name", + "position", + "permission_overwrites", + ) + + def __init__(self, data: GuildChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) + + def _update(self, data: GuildChannelData) -> None: + super()._update(data) + self.name: str = cast(str, data["name"]) + self.position: int = cast(int, data["position"]) + self.permission_overwrites: list[PermissionOverwrite] = [ + PermissionOverwrite.from_data(ow) for ow in overwrites + ] if (overwrites := data.get("permission_overwrites")) else [] + + +class PermissionOverwrite: + """ + Represents a permission overwrite for a channel. + + This has the same attributes as a :class:`Permissions` object, with the addition of an ID and type. + + Attributes + ----------- + id: :class:`int` + The ID of the overwrite. + type: :class:`str` + The type of the overwrite. + """ + __slots__ = ( + "id", + "type", + "_allow", + "_deny", + ) + + def __init__(self, obj: Snowflake, type: OverwriteType, **permissions: dict[str, bool | None]) -> None: + self.id: int = obj.id self.type: OverwriteType = type - self.allow: Permissions = allow - self.deny: Permissions = deny + # internal permission flags + self._allow: Permissions = Permissions(**{k: v if v else False for k, v in permissions.items()}) + self._deny: Permissions = Permissions(**{k: True if v else False for k, v in permissions.items()}) + + def __getattr__(self, item: str) -> bool | None: + if getattr(self._allow, item): + return True + if getattr(self._deny, item): + return False + return None + + def __setattr__(self, key: str, value: bool | None) -> None: + if value is None: + setattr(self._allow, key, False) + setattr(self._deny, key, False) + elif value: + setattr(self._allow, key, True) + setattr(self._deny, key, False) + else: + setattr(self._allow, key, False) + setattr(self._deny, key, True) @classmethod - def from_dict(cls, overwrite: DiscordOverwrite) -> _Overwrite: + def from_data(cls, data: PermissionOverwriteData) -> PermissionOverwrite: + allow_perms = int(data["allow"]) + deny_perms = int(data["deny"]) + return cls( - int(overwrite['id']), - OverwriteType(overwrite['type']), - allow=Permissions.from_value(overwrite['allow']), - deny=Permissions.from_value(overwrite['deny']), + Object(int(data["id"])), + OverwriteType(data["type"]), + **{ + k: True if allow_perms & (1 << i) else False if deny_perms & (1 << i) else None + for k, i in enumerate(Permissions._bitwise_names) + }, ) - def to_dict(self) -> DiscordOverwrite: + def to_data(self) -> PermissionOverwriteData: return { - 'id': self.id, - 'type': self.type, - 'allow': self.allow.value, - 'deny': self.deny.value, + "id": self.id, + "type": self.type.value, + "allow": str(self.allow.as_bit), + "deny": str(self.deny.as_bit), } -class ThreadMetadata: +class TextEnabledChannel(GuildChannel, Messageable): + # You can text in this one! (still a guild channel) __slots__ = ( - 'archived', - 'auto_archive_duration', - 'archive_timestamp', - 'locked', - 'invitable', - 'create_timestamp', + "nsfw", + "last_message_id", ) - def __init__(self, metadata: DiscordThreadMetadata) -> None: - self.archived: bool = metadata['archived'] - self.auto_archive_duration: int = metadata['auto_archive_duration'] - self.archive_timestamp: datetime = datetime.fromisoformat( - metadata['archive_timestamp'] - ) - self.locked: bool = metadata['locked'] - self.invitable: bool | MissingEnum = metadata.get('invitable', MISSING) - self.create_timestamp: datetime | MissingEnum = ( - datetime.fromisoformat(metadata.get('create_timestamp')) - if metadata.get('create_timestamp') is not None - else MISSING - ) + def __init__( + self, data: TextChannelData | NewsChannelData | ThreadChannelData | VoiceChannelData, state: State + ) -> None: + super().__init__(data, state) + self._update(data) + def _update(self, data: TextChannelData | NewsChannelData | ThreadChannelData | VoiceChannelData) -> None: + super()._update(data) + self.nsfw: bool = cast(bool, data.get("nsfw", False)) + self.last_message_id: int | None = int(lmid) \ + if (lmid := cast(int | None, data.get("last_message_id"))) else None -class ThreadMember: - __slots__ = ('id', 'user_id', 'join_timestamp', 'flags', 'member') - def __init__(self, member: DiscordThreadMember) -> None: - _id: str | None = member.get('id') - self.id: Snowflake | MissingEnum = ( - Snowflake(_id) if _id is not None else MISSING - ) - _user_id: str | None = member.get('user_id') - self.user_id: Snowflake | MissingEnum = ( - Snowflake(_user_id) if _user_id is not None else MISSING - ) - self.join_timestamp: datetime = datetime.fromisoformat(member['join_timestamp']) - self.flags: int = member['flags'] - self.member: Member | None = ( - Member(member['member']) if 'member' in member else None - ) +class ChildChannel(BaseChannel): + # This one can either have parents or be orphaned (loser) + __slots__ = ( + "parent_id", + ) + def __init__(self, data: GuildChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) -class ForumTag: - __slots__ = ('id', 'name', 'moderated', 'emoji_id', 'emoji_name') + def _update(self, data: GuildChannelData) -> None: + super()._update(data) + self.parent_id: int | None = int(pid) if (pid := cast(int, data.get("parent_id"))) else None + + +class _ThreadEnabled(ChildChannel, GuildChannel): + # little thread support + __slots__ = ( + "default_auto_archive_duration", + ) def __init__( + self, data: TextChannelData | NewsChannelData | ForumChannelData | MediaChannelData, state: State + ) -> None: + ChildChannel.__init__(self, data, state) + GuildChannel.__init__(self, data, state) + self._update(data) + + def _update(self, data: TextChannelData | NewsChannelData | ForumChannelData | MediaChannelData) -> None: + ChildChannel._update(self, data) + GuildChannel._update(self, data) + self.default_auto_archive_duration: Maybe[int] = cast( + Maybe[int], data.get("default_auto_archive_duration", MISSING) + ) + + async def list_public_archived_threads( self, + before: Maybe[datetime] = MISSING, + limit: Maybe[int] = MISSING, + ) -> list[Thread]: + data = await self._state.http.list_public_archived_threads( + self.id, before.isoformat() if before else MISSING, limit + ) + threads = {t["id"]: Thread(t, self._state) for t in data["threads"]} + for member in data["members"]: + threads[member["id"]].member = ThreadMember(member, self._state) + return list(threads.values()) + + +class ThreadEnabledChannel(_ThreadEnabled): + # you can make threads + async def start_thread_from_message( + self, + message: Snowflake, *, name: str, - moderated: bool = False, - emoji_id: Snowflake | str | None = None, - emoji_name: str | None = None, - ) -> None: - self.id: Snowflake | None = None - self.name: str = name - self.moderated: bool = moderated - self.emoji_id: Snowflake | str = emoji_id - self.emoji_name: str | None = emoji_name + auto_archive_duration: Maybe[int] = MISSING, + rate_limit_per_user: Maybe[int] = MISSING, + reason: str | None = None, + ) -> Thread: + data = await self._state.http.start_thread_with_message( + self.id, + message, + name=name, + auto_archive_duration=auto_archive_duration, + rate_limit_per_user=rate_limit_per_user, + reason=reason, + ) + return Thread(data, self._state) - @classmethod - def from_dict(cls, data: DiscordForumTag) -> ForumTag: - obj = cls( - name=data['name'], - moderated=data['moderated'], - emoji_id=Snowflake(em_id) - if isinstance(em_id := data.get('emoji_id'), int) - else em_id, - emoji_name=data.get('emoji_name'), + async def start_thread_without_message( + self, + *, + name: str, + auto_archive_duration: Maybe[int] = MISSING, + type: Maybe[ChannelType] = MISSING, + invitable: Maybe[bool] = MISSING, + rate_limit_per_user: Maybe[int] = MISSING, + ) -> Thread: + data = await self._state.http.start_thread_without_message( + self.id, + name=name, + auto_archive_duration=auto_archive_duration, + type=type, + invitable=invitable, + rate_limit_per_user=rate_limit_per_user, ) - obj.id = Snowflake(data['id']) - return obj + return Thread(data, self._state) - def to_dict(self) -> DiscordForumTag: - # omit ID because we don't send that to Discord - return { - 'name': self.name, - 'moderated': self.moderated, - 'emoji_id': self.emoji_id, - 'emoji_name': self.emoji_name, - } + async def list_private_archived_threads( + self, + before: datetime | None = None, + limit: int | None = None, + ) -> list[Thread]: + data = await self._state.http.list_private_archived_threads(self.id, before=before, limit=limit) + threads = {t["id"]: Thread(t, self._state) for t in data["threads"]} + for member in data["members"]: + threads[member["id"]].member = ThreadMember(member, self._state) + return list(threads.values()) + async def list_joined_private_archived_threads( + self, + before: datetime | None = None, + limit: int | None = None, + ) -> list[Thread]: + data = await self._state.http.list_joined_private_archived_threads(self.id, before=before, limit=limit) + threads = {t["id"]: Thread(t, self._state) for t in data["threads"]} + for member in data["members"]: + threads[member["id"]].member = ThreadMember(member, self._state) + return list(threads.values()) -class DefaultReaction: - __slots__ = ('emoji_id', 'emoji_name') - def __init__(self, data: DiscordDefaultReaction) -> None: - _emoji_id: str | None = data.get('emoji_id') - self.emoji_id: Snowflake | None = ( - Snowflake(_emoji_id) if _emoji_id is not None else None +class ThreadBasedChannel(_ThreadEnabled): + # this one is only threads + __slots__ = ( + "topic", + "nsfw", + "last_message_id", + "rate_limit_per_user", + "available_tags", + "default_reaction_emoji", + "default_thread_rate_limit_per_user", + "available_tags", + "default_sort_order", + "default_forum_layout", + ) + + def __init__(self, data: ForumChannelData | MediaChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) + + def _update(self, data: ForumChannelData | MediaChannelData) -> None: + super()._update(data) + self.topic: str | None = data["topic"] + self.nsfw: bool = data["nsfw"] + self.last_message_id: int | None = int(lmid) if (lmid := data.get("last_message_id")) else None + self.rate_limit_per_user: int = data["rate_limit_per_user"] + self.available_tags: list[ForumTag] = [ForumTag(tag) for tag in data["available_tags"]] + self.default_reaction_emoji: DefaultReaction | None = DefaultReaction(data["default_reaction_emoji"]) if ( + data.get("default_reaction_emoji")) else None + self.default_thread_rate_limit_per_user: int = data["default_thread_rate_limit_per_user"] + self.default_sort_order: SortOrderType | None = SortOrderType(data["default_sort_order"]) if ( + data.get("default_sort_order")) else None + self.default_forum_layout: ForumLayoutType = ForumLayoutType(data["default_forum_layout"]) + + async def start_thread( + self, + name: str, + *, + content: Maybe[str], + embeds: Maybe[list[Embed]] = MISSING, + files: Maybe[list[File]] = MISSING, + allowed_mentions: Maybe[AllowedMentions] = MISSING, + components: Maybe[list[...]] = MISSING, # TODO: Components + stickers: Maybe[list[Snowflake]] = MISSING, + attachment: Maybe[Attachment] = MISSING, + flags: Maybe[MessageFlags] = MISSING, + auto_archive_duration: Maybe[int] = MISSING, + rate_limit_per_user: Maybe[int] = MISSING, + applied_tags: Maybe[list[Snowflake]] = MISSING, + reason: str | None = None, + ) -> Thread: + message = { + "content": content, + "embeds": [e.to_dict() for e in embeds] if embeds else MISSING, + "allowed_mentions": allowed_mentions.to_dict() if allowed_mentions else MISSING, + "components": components, + "stickers": stickers, + "attachment": attachment, + "flags": flags, + } + data = await self._state.http.start_thread_in_forum_channel( + self.id, + name=name, + message=message, + auto_archive_duration=auto_archive_duration, + rate_limit_per_user=rate_limit_per_user, + applied_tags=[tag.id for tag in applied_tags] if applied_tags else MISSING, + files=files, + reason=reason, ) - self.emoji_name: str | None = data['emoji_name'] + return Thread(data, self._state) -class FollowedChannel: - __slots__ = ('channel_id', 'webhook_id') +class ForumTag: + """ + A tag for a forum channel. + + Attributes + ----------- + id: :class:`int` + The ID of the tag. + name: :class:`str` + The name of the tag. + moderated: :class:`bool` + Whether the tag is moderated. + emoji_id: :class:`int` + The ID of the emoji for the tag. + emoji_name: :class:`str` + The name of the emoji for the tag. + """ + __slots__ = ( + "id", + "name", + "moderated", + "emoji_id", + "emoji_name", + ) - def __init__(self, data: dict[str, Any]) -> None: - self.channel_id: Snowflake = Snowflake(data['channel_id']) - self.webhook_id: Snowflake = Snowflake(data['webhook_id']) + def __init__(self, data: ForumTagData) -> None: + self.id: int = int(data["id"]) + self.name: str = data["name"] + self.moderated: bool = data["moderated"] + self.emoji_id: int | None = int(eid) if (eid := data.get("emoji_id")) else None + self.emoji_name: str | None = data.get("emoji_name") -class Channel: - """Represents a Discord channel. All other channel types inherit from this. +class DefaultReaction: + """ + The default reaction for a thread-based channel. Attributes - ---------- - id: :class:`Snowflake` - The ID of the channel. - type: :class:`ChannelType` - The type of the channel. - name: :class:`str` | :class:`MissingEnum` - The name of the channel. - flags: :class:`ChannelFlags` | :class:`MissingEnum` - The flags of the channel. + ----------- + emoji_id: :class:`int` + The ID of the emoji. + emoji_name: :class:`str` + The name of the emoji. """ + __slots__ = ( + "emoji_id", + "emoji_name", + ) - __slots__ = ('_state', 'id', 'type', 'name', 'flags') + def __init__(self, data: DefaultReactionData) -> None: + self.emoji_id: int | None = int(eid) if (eid := data.get("emoji_id")) else None + self.emoji_name: str | None = data.get("emoji_name") - def __init__(self, data: DiscordChannel, state: State) -> None: - self._state = state - self.id: Snowflake = Snowflake(data['id']) - self.type: ChannelType = ChannelType(data['type']) - self.name: str | MissingEnum = data.get('name', MISSING) - self.flags: ChannelFlags | MissingEnum = ( - ChannelFlags.from_value(data['flags']) - if data.get('flags') is not None - else MISSING - ) - async def _base_edit(self, **kwargs: Any) -> Channel: - data = await self._state.http.modify_channel(self.id, **kwargs) - return self.__class__(data, self._state) +class VoiceEnabledChannel(ChildChannel, GuildChannel): + # you can vocally speak! + __slots__ = ( + "bitrate", + "user_limit", + ) - async def delete(self, reason: str | None = None) -> None: - """Delete this channel. + def __init__(self, data: VoiceChannelData, state: State) -> None: + ChildChannel.__init__(self, data, state) + GuildChannel.__init__(self, data, state) + self._update(data) - Parameters - ---------- - reason: :class:`str` | None - The reason for deleting this channel. Shows up on the audit log. - """ - await self._state.http.delete_channel(self.id, reason=reason) + def _update(self, data: VoiceChannelData) -> None: + ChildChannel._update(self, data) + GuildChannel._update(self, data) + self.bitrate: int = data["bitrate"] + self.user_limit: int = data["user_limit"] + self.rtc_region: str | None = data["rtc_region"] + self.video_quality_mode: Maybe[VideoQualityMode] = VideoQualityMode(vqm) if ( + vqm := data.get("video_quality_mode")) else MISSING -class GuildChannel(Channel): - """Represents a Discord guild channel. All other guild channel types inherit from this. +class FollowedChannel: + """ + Represents a followed channel. Attributes - ---------- - id: :class:`Snowflake` + ----------- + channel_id: :class:`int` The ID of the channel. + webhook_id: :class:`int` + The ID of the webhook. + """ + __slots__ = ( + "channel_id", + "webhook_id", + ) + + def __init__(self, data: FollowedChannelData) -> None: + self.channel_id: int = int(data["channel_id"]) + self.webhook_id: int = int(data["webhook_id"]) + + +# Real channel types, finally +class TextChannel(TextEnabledChannel, ThreadEnabledChannel): + """ + Represents a guild text channel. + + Attributes + ----------- type: :class:`ChannelType` - The type of the channel. - name: :class:`str` | :class:`MissingEnum` - The name of the channel. - flags: :class:`ChannelFlags` | :class:`MissingEnum` - The flags of the channel. - guild_id: :class:`Snowflake` | :class:`MissingEnum` - The ID of the guild this channel belongs to. - position: :class:`int` | :class:`MissingEnum` - The position of the channel. - permission_overwrites: list[:class:`_Overwrite`] - The permission overwrites of the channel. - topic: :class:`str` | None | :class:`MissingEnum` - The topic of the channel. - nsfw: :class:`bool` | :class:`MissingEnum` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: List[:class:`PermissionOverwrite`] + The channel's permission overwrites. + nsfw: :class:`bool` Whether the channel is NSFW. - permissions: :class:`Permissions` | :class:`MissingEnum` - The bot's permissions in this channel. - parent_id: :class:`Snowflake` | None | :class:`MissingEnum` - The ID of the parent category of this channel. - rate_limit_per_user: :class:`int` | :class:`MissingEnum` - The slowmode rate limit of the channel. - default_auto_archive_duration: :class:`int` | :class:`MissingEnum` - The default auto archive duration of the channel. + last_message_id: Optional[:class:`int`] + The ID of the last message sent in the channel. + parent_id: Optional[:class:`int`] + The ID of the category this channel belongs to. + topic: Optional[:class:`str`] + The channel's topic. + rate_limit_per_user: :class:`int` + The rate limit per user in seconds. + last_pin_timestamp: Optional[:class:`datetime.datetime`] + The timestamp of the last pinned message. + default_auto_archive_duration: Optional[:class:`int`] + The default duration for newly created threads in minutes. + """ + type: ChannelType.GUILD_TEXT __slots__ = ( - 'guild_id', - 'position', - 'permission_overwrites', - 'topic', - 'nsfw', - 'permissions', - 'parent_id', - 'rate_limit_per_user', - 'default_auto_archive_duration', + "topic", + "rate_limit_per_user", + "last_pin_timestamp", + "default_auto_archive_duration", ) - def __init__(self, data: DiscordChannel, state: State) -> None: - super().__init__(data, state) - self.guild_id: Snowflake | MissingEnum = ( - Snowflake(data['guild_id']) if data.get('guild_id') is not None else MISSING - ) - self.position: int | MissingEnum = data.get('position', MISSING) - self.permission_overwrites: list[_Overwrite] = [ - _Overwrite.from_dict(d) for d in data.get('permission_overwrites', []) - ] - self.topic: str | None | MissingEnum = data.get('topic', MISSING) - self.nsfw: bool | MissingEnum = data.get('nsfw', MISSING) - self.permissions: Permissions | MissingEnum = ( - Permissions.from_value(data['permissions']) - if data.get('permissions') is not None - else MISSING - ) - self.parent_id: Snowflake | MissingEnum = ( - Snowflake(data['parent_id']) - if data.get('parent_id') is not None - else data.get('parent_id', MISSING) - ) - self.rate_limit_per_user: int | MissingEnum = data.get( - 'rate_limit_per_user', MISSING - ) - self.default_auto_archive_duration: int | MissingEnum = data.get( - 'default_auto_archive_duration', MISSING - ) + def __init__(self, data: TextChannelData, state: State) -> None: + TextEnabledChannel.__init__(self, data, state) + ThreadEnabledChannel.__init__(self, data, state) + self._update(data) + def _update(self, data: TextChannelData) -> None: + TextEnabledChannel._update(self, data) + ThreadEnabledChannel._update(self, data) + self.topic: str | None = data["topic"] + self.rate_limit_per_user: int = data["rate_limit_per_user"] + self.last_pin_timestamp: Maybe[datetime | None] = datetime.fromisoformat(pints) if ( + (pints := data.get("last_pin_timestamp", MISSING)) not in (MISSING, None)) else pints -class MessageableChannel(Channel): - def __init__(self, data: DiscordChannel, state: State) -> None: - super().__init__(data, state) - self.last_message_id: int | None = ( - Snowflake(data['last_message_id']) - if data.get('last_message_id') is not None - else None - ) - self.last_pin_timestamp: None | datetime | MissingEnum = ( - datetime.fromisoformat(data['last_pin_timestamp']) - if data.get('last_pin_timestamp') is not None - else data.get('last_pin_timestamp', MISSING) - ) - - async def send( + async def modify( self, - content: str | MissingEnum = MISSING, *, - nonce: int | str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - allowed_mentions: AllowedMentions | MissingEnum = MISSING, - message_reference: MessageReference | MissingEnum = MISSING, - sticker_ids: list[Snowflake] | MissingEnum = MISSING, - files: list[File] | MissingEnum = MISSING, - flags: int | MissingEnum = MISSING, - houses: list[House] | MissingEnum = MISSING, - ) -> Message: - if houses: - if len(houses) > 5: - raise ComponentException('Cannot have over five houses at once') - - components = [(house.action_row())._to_dict() for house in houses] - - for house in houses: - self._state.sent_house(house) - else: - components = MISSING - - data = await self._state.http.create_message( - channel_id=self.id, - content=content, - nonce=nonce, - tts=tts, - embeds=embeds, - allowed_mentions=allowed_mentions.to_dict() - if allowed_mentions - else MISSING, - message_reference=message_reference.to_dict() - if message_reference - else MISSING, - sticker_ids=sticker_ids, - flags=flags, - components=components, - files=files, - ) - return Message(data, state=self._state) + name: Maybe[str] = MISSING, + type: Maybe[ChannelType] = MISSING, + position: Maybe[int | None] = MISSING, + topic: Maybe[str | None] = MISSING, + nsfw: Maybe[bool | None] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwrite] | None] = MISSING, + parent_id: Maybe[int | None] = MISSING, + default_auto_archive_duration: Maybe[int | None] = MISSING, + default_thread_rate_limit_per_user: Maybe[int] = MISSING, + reason: str | None = None, + ) -> Self: + payload = { + "name": name, + "type": type.value, + "position": position, + "topic": topic, + "nsfw": nsfw, + "rate_limit_per_user": rate_limit_per_user, + "permission_overwrites": [p.to_data() for p in permission_overwrites] if permission_overwrites else MISSING, + "parent_id": parent_id, + "default_auto_archive_duration": default_auto_archive_duration, + "default_thread_rate_limit_per_user": default_thread_rate_limit_per_user, + } + return await self._modify(reason=reason, **payload) - @cached_property - def typing(self) -> Typing: - return Typing(self.id, self._state) + async def get_pinned_messages(self) -> list[Message]: + data = await self._state.http.get_pinned_messages(self.id) + return [Message(m, self._state) for m in data] - async def bulk_delete(self, *messages: Message, reason: str | None = None) -> None: - await self._state.http.bulk_delete_messages( - self.id, [m.id for m in messages], reason=reason - ) - async def bulk_delete_ids( - self, *messages: Snowflake, reason: str | None = None - ) -> None: - await self._state.http.bulk_delete_messages(self.id, messages, reason=reason) +class DMChannel(BaseChannel, Messageable): + """ + Represents a direct message channel. + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + recipients: List[:class:`User`] + The recipients of the channel. + last_message_id: Optional[:class:`int`] + The ID of the last message sent in the channel. + last_pin_timestamp: Optional[:class:`datetime.datetime`] + The timestamp of the last pinned message. + """ + type: ChannelType.DM -class AudioChannel(GuildChannel): - def __init__(self, data: DiscordChannel, state: State) -> None: - super().__init__(data, state) - self.rtc_region: str | MissingEnum = data.get('rtc_region', MISSING) - self.video_quality_mode: VideoQualityMode | MissingEnum = ( - VideoQualityMode(data['video_quality_mode']) - if data.get('video_quality_mode') is not None - else MISSING - ) - self.bitrate: int | MissingEnum = data.get('bitrate', MISSING) - self.user_limit: int | MissingEnum = data.get('user_limit', MISSING) + __slots__ = ( + "recipients", + "last_message_id", + "last_pin_timestamp", + ) + def __init__(self, data: DMChannelData | GroupDMChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) -class TextChannel(MessageableChannel, GuildChannel): - # Type 0 - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - type: ChannelType | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - topic: str | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | MissingEnum = MISSING, - parent_id: Snowflake | None | MissingEnum = MISSING, - default_auto_archive_duration: int | None | MissingEnum = MISSING, - default_thread_rate_limit_per_user: int | MissingEnum = MISSING, - ) -> TextChannel: - return await self._base_edit( - name=name, - type=type.value if type else MISSING, - position=position, - topic=topic, - nsfw=nsfw, - rate_limit_per_user=rate_limit_per_user, - permission_overwrites=[o.to_dict() for o in permission_overwrites] - if permission_overwrites - else MISSING, - parent_id=parent_id, - default_auto_archive_duration=default_auto_archive_duration, - default_thread_rate_limit_per_user=default_thread_rate_limit_per_user, - ) + def _update(self, data: DMChannelData | GroupDMChannelData) -> None: + super()._update(data) + self.last_message_id: int | None = int(cast(str, lmid)) if (lmid := data.get("last_message_id")) else None + self.recipients: list[User] = [User(user, self._state) for user in data["recipients"]] + self.last_pin_timestamp: Maybe[datetime | None] = datetime.fromisoformat(cast(str, lpts)) if ( + (lpts := data.get("last_pin_timestamp", MISSING)) not in (MISSING, None)) else cast(Maybe[None], lpts) async def get_pinned_messages(self) -> list[Message]: data = await self._state.http.get_pinned_messages(self.id) - return [Message(m, state=self._state) for m in data] + return [Message(m, self._state) for m in data] + - async def create_thread( +class VoiceChannel(VoiceEnabledChannel, TextEnabledChannel): + """ + Represents a guild voice channel. + + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: List[:class:`PermissionOverwrite`] + The channel's permission overwrites. + bitrate: :class:`int` + The channel's bitrate. + user_limit: :class:`int` + The channel's user limit. + rtc_region: Optional[:class:`str`] + The channel's RTC region. + video_quality_mode: Optional[:class:`VideoQualityMode`] + The channel's video quality mode. + parent_id: Optional[:class:`int`] + The ID of the category this channel belongs to. + nsfw: :class:`bool` + Whether the channel is NSFW. + last_message_id: Optional[:class:`int`] + The ID of the last message sent in the channel. + """ + type: ChannelType.GUILD_VOICE + + def __init__(self, data: VoiceChannelData, state: State) -> None: + VoiceEnabledChannel.__init__(self, data, state) + TextEnabledChannel.__init__(self, data, state) + self._update(data) + + async def modify( self, - message: Message | None = None, *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - type: ChannelType = ChannelType.PUBLIC_THREAD, - invitable: bool | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, + name: Maybe[str] = MISSING, + position: Maybe[int | None] = MISSING, + nsfw: Maybe[bool | None] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + bitrate: Maybe[int | None] = MISSING, + user_limit: Maybe[int | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwrite] | None] = MISSING, + parent_id: Maybe[int | None] = MISSING, + rtc_region: Maybe[str | None] = MISSING, + video_quality_mode: Maybe[VideoQualityMode | None] = MISSING, reason: str | None = None, - ) -> Thread | AnnouncementThread: - if message: - data = await self._state.http.start_thread_from_message( - self.id, - message.id, - name=name, - auto_archive_duration=auto_archive_duration, - rate_limit_per_user=rate_limit_per_user, - reason=reason, - ) - else: - data = await self._state.http.start_thread_without_message( - self.id, - name=name, - auto_archive_duration=auto_archive_duration, - type=type.value, - invitable=invitable, - rate_limit_per_user=rate_limit_per_user, - reason=reason, - ) - return identify_channel(data, state=self._state) + ) -> Self: + payload = { + "name": name, + "position": position, + "nsfw": nsfw, + "rate_limit_per_user": rate_limit_per_user, + "bitrate": bitrate, + "user_limit": user_limit, + "permission_overwrites": [p.to_data() for p in permission_overwrites] if permission_overwrites else MISSING, + "parent_id": parent_id, + "rtc_region": rtc_region, + "video_quality_mode": video_quality_mode.value if video_quality_mode else MISSING, + } + return await self._modify(reason=reason, **payload) - async def list_public_archived_threads( - self, - before: datetime.datetime | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> list[Thread]: - data = await self._state.http.list_public_archived_threads( - self.id, - before=before.isoformat() if before else MISSING, - limit=limit, - ) - return [Thread(d, state=self._state) for d in data] + async def connect(self) -> None: + # TODO: implement + raise NotImplementedError - async def list_private_archived_threads( - self, - before: datetime.datetime | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> list[Thread]: - data = await self._state.http.list_private_archived_threads( - self.id, - before=before.isoformat() if before else MISSING, - limit=limit, - ) - return [Thread(d, state=self._state) for d in data] - - async def list_joined_private_archived_threads( - self, - before: datetime.datetime | MissingEnum = MISSING, - limit: int | MissingEnum = MISSING, - ) -> list[Thread]: - data = await self._state.http.list_joined_private_archived_threads( - self.id, - before=before.isoformat() if before else MISSING, - limit=limit, - ) - return [Thread(d, state=self._state) for d in data] +class GroupDMChannel(DMChannel): + """ + Represents a group direct message channel. -class DMChannel(MessageableChannel): - # Type 1 - ... + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + icon_hash: Optional[:class:`str`] + The hash of the channel's icon. + owner_id: :class:`int` + The ID of the channel's owner. + application_id: Optional[:class:`int`] + The ID of the channel's application. + """ + type: ChannelType.GROUP_DM + __slots__ = ( + "name", + "icon_hash", + "owner_id", + "application_id", + ) -class VoiceChannel(MessageableChannel, AudioChannel): - # Type 2 - async def edit( + def __init__(self, data: GroupDMChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) + + def _update(self, data: GroupDMChannelData) -> None: + super()._update(data) + self.name: str = data["name"] + self.icon_hash: str | None = data["icon"] + self.owner_id: int = int(data["owner_id"]) + self.application_id: int | None = int(appid) if (appid := data.get("application_id")) else None + + @property + def icon(self) -> Asset | None: + """:class:`Asset` | None: Returns the channel's icon asset if it exists.""" + # TODO: i don't know what route this uses, it's not documented + return + + async def modify( self, *, - name: str | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - bitrate: int | None | MissingEnum = MISSING, - user_limit: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | MissingEnum = MISSING, - parent_id: Snowflake | None | MissingEnum = MISSING, - rtc_region: str | None | MissingEnum = MISSING, - video_quality_mode: VideoQualityMode | None | MissingEnum = MISSING, - ) -> VoiceChannel: - return await self._base_edit( - name=name, - position=position, - nsfw=nsfw, - bitrate=bitrate, - user_limit=user_limit, - permission_overwrites=[o.to_dict() for o in permission_overwrites] - if permission_overwrites - else MISSING, - parent_id=parent_id, - rtc_region=rtc_region, - video_quality_mode=video_quality_mode.value - if video_quality_mode - else MISSING, - ) + name: Maybe[str] = MISSING, + icon: Maybe[bytes] = MISSING, + ) -> Self: + return await self._modify(name=name, icon=icon) + async def add_recipient(self, user: Snowflake, access_token: str, nick: str) -> None: + # TODO: implement + raise NotImplementedError -class GroupDMChannel(MessageableChannel): - # Type 3 - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - icon: str | MissingEnum = MISSING, - ) -> GroupDMChannel: - data = await self._state.http.modify_channel(self.id, name=name, icon=icon) - return GroupDMChannel(data, self._state) + async def remove_recipient(self, user: Snowflake) -> None: + # TODO: implement + raise NotImplementedError + + async def get_pinned_messages(self): + # TODO: implement + raise NotImplementedError + + +class CategoryChannel(GuildChannel): + """ + Represents a guild category channel. + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: List[:class:`PermissionOverwrite`] + The channel's permission overwrites. + """ + type: ChannelType.GUILD_CATEGORY -class CategoryChannel(Channel): - # Type 4 - async def edit( + async def modify( self, *, - name: str | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | MissingEnum = MISSING, - ) -> CategoryChannel: - return await self._base_edit( - name=name, - position=position, - permission_overwrites=[o.to_dict() for o in permission_overwrites] - if permission_overwrites - else MISSING, - ) + name: Maybe[str] = MISSING, + position: Maybe[int | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwrite] | None] = MISSING, + reason: str | None = None, + ) -> Self: + payload = { + "name": name, + "position": position, + "permission_overwrites": [p.to_data() for p in permission_overwrites] if permission_overwrites else MISSING, + } + return await self._modify(reason=reason, **payload) -class AnnouncementChannel(TextChannel): - # Type 5 - async def follow(self, target_channel: TextChannel) -> FollowedChannel: - data = await self._state.http.follow_news_channel(self.id, target_channel.id) - return FollowedChannel(data) +class NewsChannel(TextEnabledChannel, ThreadEnabledChannel): + """ + Represents a guild announcement channel. - async def create_thread( + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: list[:class:`PermissionOverwrite`] + The channel's permission overwrites. + nsfw: :class:`bool` + Whether the channel is NSFW. + last_message_id: :class:`int` | None + The ID of the last message sent in the channel. + topic: :class:`str` | None + The channel's topic. + last_pin_timestamp: :class:`datetime.datetime` | None + The timestamp of the last pinned message. + default_auto_archive_duration: :class:`int` | None + The default duration for newly created threads in minutes. + """ + type: ChannelType.GUILD_ANNOUNCEMENT + + __slots__ = ( + "topic", + "last_pin_timestamp", + "default_auto_archive_duration", + ) + + def __init__(self, data: NewsChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) + + def _update(self, data: NewsChannelData) -> None: + super()._update(data) + self.topic: str | None = data["topic"] + self.last_pin_timestamp: Maybe[datetime | None] = datetime.fromisoformat(lpts) if ( + (lpts := data.get("last_pin_timestamp", MISSING)) not in (MISSING, None)) else lpts + self.default_auto_archive_duration: Maybe[int] = data.get("default_auto_archive_duration", MISSING) + + async def modify( self, - message: Message | None = None, *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - invitable: bool | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, + name: Maybe[str] = MISSING, + type: Maybe[ChannelType] = MISSING, + position: Maybe[int | None] = MISSING, + topic: Maybe[str | None] = MISSING, + nsfw: Maybe[bool | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwrite] | None] = MISSING, + parent_id: Maybe[int | None] = MISSING, + default_auto_archive_duration: Maybe[int | None] = MISSING, reason: str | None = None, - ) -> Thread: - return await super().create_thread( - message, - name=name, - auto_archive_duration=auto_archive_duration, - type=ChannelType.ANNOUNCEMENT_THREAD, - invitable=invitable, - rate_limit_per_user=rate_limit_per_user, - reason=reason, - ) + ) -> Self: + payload = { + "name": name, + "type": type.value, + "position": position, + "topic": topic, + "nsfw": nsfw, + "permission_overwrites": [p.to_data() for p in permission_overwrites] if permission_overwrites else MISSING, + "parent_id": parent_id, + "default_auto_archive_duration": default_auto_archive_duration, + } + return await self._modify(reason=reason, **payload) + async def get_pinned_messages(self) -> list[Message]: + data = await self._state.http.get_pinned_messages(self.id) + return [Message(m, self._state) for m in data] -class Thread(MessageableChannel, GuildChannel): - # Type 11 & 12 - def __init__(self, data: DiscordChannel, state: State) -> None: - super().__init__(data, state) - self.default_thread_rate_limit_per_user: int | MissingEnum = data.get( - 'default_thread_rate_limit_per_user', MissingEnum - ) - self.message_count: int | MissingEnum = data.get('message_count', MISSING) - self.thread_metadata: ThreadMetadata | MissingEnum = ( - ThreadMetadata(data['thread_metadata']) - if data.get('thread_metadata') is not None - else MISSING - ) - self.owner_id: Snowflake | MissingEnum = ( - Snowflake(data['owner_id']) if data.get('owner_id') is not None else MISSING - ) + async def follow(self, webhook_channel: Snowflake) -> FollowedChannel: + data = await self._state.http.follow_announcement_channel(self.id, webhook_channel=webhook_channel.id) + return FollowedChannel(data) - async def edit( + +class Thread(ChildChannel, Messageable): + """ + Represents a guild thread channel. + + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + last_message_id: :class:`int` | None + The ID of the last message sent in the channel. + rate_limit_per_user: :class:`int` + The rate limit per user in seconds. + owner_id: :class:`int` + The ID of the thread's owner. + last_pin_timestamp: :class:`datetime.datetime` | None + The timestamp of the last pinned message. + message_count: :class:`int` + The number of messages in the thread. + member_count: :class:`int` + The number of members in the thread. + thread_metadata: :class:`ThreadMetadata` + The metadata of the thread. + member: :class:`ThreadMember` | None + The member object of the current user if they are a member of the thread. + total_messages_sent: :class:`int` + The total number of messages sent in the thread. + applied_tags: list[:class:`Snowflake`] + The tags applied to the thread. + parent_id: :class:`int` | None + The ID of the channel this thread belongs to. + """ + type: Union[ChannelType.ANNOUNCEMENT_THREAD, ChannelType.PUBLIC_THREAD, ChannelType.PRIVATE_THREAD] + + __slots__ = ( + "name", + "last_message_id", + "rate_limit_per_user", + "owner_id", + "last_pin_timestamp", + "message_count", + "member_count", + "thread_metadata", + "member", + "total_messages_sent", + "applied_tags", + ) + + def __init__(self, data: ThreadChannelData, state: State) -> None: + super().__init__(data, state) + self._update(data) + + def _update(self, data: ThreadChannelData) -> None: + super()._update(data) + self.name: str = data["name"] + self.last_message_id: int | None = int(lmid) if (lmid := data.get("last_message_id")) else None + self.rate_limit_per_user: int = data["rate_limit_per_user"] + self.owner_id: int = int(data["owner_id"]) + self.last_pin_timestamp: Maybe[datetime | None] = datetime.fromisoformat(lpts) if ( + (lpts := data.get("last_pin_timestamp", MISSING)) not in (MISSING, None)) else lpts + self.message_count: int = data["message_count"] + self.member_count: int = data["member_count"] + self.thread_metadata: ThreadMetadata = ThreadMetadata(data["thread_metadata"]) + self.member: ThreadMember | None = ThreadMember(data["member"], self._state) if (data.get("member")) else None + self.total_messages_sent: int = data["total_messages_sent"] + self.applied_tags: list[Snowflake] = [Object(tag) for tag in data["applied_tags"]] + + @property + def is_private(self) -> bool: + return self.type == ChannelType.PRIVATE_THREAD + + async def modify( self, *, - name: str | MissingEnum = MISSING, - archived: bool | MissingEnum = MISSING, - auto_archive_duration: int | MissingEnum = MISSING, - locked: bool | MissingEnum = MISSING, - invitable: bool | MissingEnum = MISSING, - rate_limit_per_user: int | MissingEnum = MISSING, - flags: ChannelFlags | MissingEnum = MISSING, - applied_tags: list[ForumTag] | MissingEnum = MISSING, - ) -> Thread: - return await self._base_edit( - name=name, - archived=archived, - auto_archive_duration=auto_archive_duration, - locked=locked, - invitable=invitable, - rate_limit_per_user=rate_limit_per_user, - flags=flags.value if flags else MISSING, - applied_tags=[t.id for t in applied_tags] if applied_tags else MISSING, - ) + name: Maybe[str] = MISSING, + archived: Maybe[bool] = MISSING, + auto_archive_duration: Maybe[int] = MISSING, + locked: Maybe[bool] = MISSING, + invitable: Maybe[bool] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + flags: Maybe[ChannelFlags] = MISSING, + applied_tags: Maybe[list[Snowflake]] = MISSING, + ) -> Self: + payload = { + "name": name, + "archived": archived, + "auto_archive_duration": auto_archive_duration, + "locked": locked, + "invitable": invitable, + "rate_limit_per_user": rate_limit_per_user, + "flags": flags, + "applied_tags": [tag.id for tag in applied_tags] if applied_tags else MISSING, + } + return await self._modify(**payload) async def join(self) -> None: - await self._state.http.join_thread(self.id) + return await self._state.http.join_thread(self.id) - async def add_member(self, member: Member) -> None: - await self._state.http.add_thread_member(self.id, member.id) + async def add_member(self, user: Snowflake) -> None: + return await self._state.http.add_thread_member(self.id, user.id) async def leave(self) -> None: - await self._state.http.leave_thread(self.id) + return await self._state.http.leave_thread(self.id) - async def remove_member(self, member: Member) -> None: - await self._state.http.remove_thread_member(self.id, member.id) + async def remove_member(self, user: Snowflake) -> None: + return await self._state.http.remove_thread_member(self.id, user.id) - async def get_member( - self, id: Snowflake, *, with_member: bool = True - ) -> ThreadMember: - data = await self._state.http.get_thread_member( - self.id, id, with_member=with_member - ) - return ThreadMember(data) + async def get_member(self, user: Snowflake, *, with_member: bool = True) -> ThreadMember: + data = await self._state.http.get_thread_member(self.id, user.id, with_member=with_member) + return ThreadMember(data, self._state) async def list_members( self, *, + limit: int = 50, + before: Snowflake = None, with_member: bool = True, - limit: int = 100, - after: Snowflake | MissingEnum = MISSING, ) -> list[ThreadMember]: - data = await self._state.http.list_thread_members( - self.id, with_member=with_member, limit=limit, after=after - ) - return [ThreadMember(d) for d in data] + data = await self._state.http.list_thread_members(self.id, limit=limit, before=before, with_member=with_member) + return [ThreadMember(member, self._state) for member in data] + async def get_pinned_messages(self) -> list[Message]: + data = await self._state.http.get_pinned_messages(self.id) + return [Message(m, self._state) for m in data] -class AnnouncementThread(Thread): - # Type 10 - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - archived: bool | MissingEnum = MISSING, - auto_archive_duration: int | MissingEnum = MISSING, - locked: bool | MissingEnum = MISSING, - rate_limit_per_user: int | MissingEnum = MISSING, - ) -> AnnouncementThread: - return await self._base_edit( - name=name, - archived=archived, - auto_archive_duration=auto_archive_duration, - locked=locked, - rate_limit_per_user=rate_limit_per_user, - ) +class ThreadMetadata: + """ + The metadata of a thread. -class StageChannel(AudioChannel, MessageableChannel): - # Type 13 - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - bitrate: int | None | MissingEnum = MISSING, - user_limit: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | MissingEnum = MISSING, - parent_id: Snowflake | None | MissingEnum = MISSING, - rtc_region: str | None | MissingEnum = MISSING, - video_quality_mode: VideoQualityMode | None | MissingEnum = MISSING, - ) -> StageChannel: - return await self._base_edit( - name=name, - position=position, - nsfw=nsfw, - bitrate=bitrate, - user_limit=user_limit, - permission_overwrites=[o.to_dict() for o in permission_overwrites] - if permission_overwrites - else MISSING, - parent_id=parent_id, - rtc_region=rtc_region, - video_quality_mode=video_quality_mode.value - if video_quality_mode - else MISSING, - ) + Attributes + ----------- + archived: :class:`bool` + Whether the thread is archived. + auto_archive_duration: :class:`int` + The duration in minutes to automatically archive the thread after recent activity. + archive_timestamp: :class:`datetime.datetime` + The timestamp of when the thread was archived. + locked: :class:`bool` + Whether the thread is locked. + invitable: :class:`bool` | None + Whether the thread is invitable. + create_timestamp: :class:`datetime.datetime` | None + The timestamp of when the thread was created. + """ + __slots__ = ( + "archived", + "auto_archive_duration", + "archive_timestamp", + "locked", + "invitable", + "create_timestamp", + ) + def __init__(self, data: ThreadMetadataData) -> None: + self.archived: bool = data["archived"] + self.auto_archive_duration: int = data["auto_archive_duration"] + self.archive_timestamp: datetime = datetime.fromisoformat(data["archive_timestamp"]) + self.locked: bool = data["locked"] + self.invitable: Maybe[bool] = data.get("invitable", MISSING) + self.create_timestamp: Maybe[datetime | None] = datetime.fromisoformat(ct) if ( + (ct := data.get("create_timestamp", MISSING)) not in (MISSING, None)) else ct -class DirectoryChannel(Channel): - # Type 14 - ... +class ThreadMember: + """ + A member of a thread. -class ForumChannel(Channel): - # Type 15 - def __init__(self, data: DiscordChannel, state: State) -> None: - super().__init__(data, state) - self.default_sort_order: int | None | MissingEnum = data.get( - 'default_sort_order', MissingEnum - ) - self.default_reaction_emoji: DefaultReaction | MissingEnum = ( - DefaultReaction(data['default_reaction_emoji']) - if data.get('default_reaction_emoji') is not None - else MISSING - ) - self.available_tags: list[ForumTag] = [ - ForumTag.from_dict(d) for d in data.get('available_tags', []) - ] + Attributes + ----------- + id: :class:`int` + The ID of the thread member. + user_id: :class:`int` + The ID of the user. + join_timestamp: :class:`datetime.datetime` + The timestamp of when the user joined the thread. + flags: :class:`int` + The flags of the thread member. + member: :class:`Member` | None + The member object of the user if they are a member of the guild. + """ + __slots__ = ( + "_state", + "id", + "user_id", + "join_timestamp", + "flags", + "member", + ) - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - topic: str | None | MissingEnum = MISSING, - nsfw: bool | None | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | MissingEnum = MISSING, - parent_id: Snowflake | None | MissingEnum = MISSING, - default_auto_archive_duration: int | None | MissingEnum = MISSING, - flags: ChannelFlags | MissingEnum = MISSING, - available_tags: list[ForumTag] | MissingEnum = MISSING, - ) -> ForumChannel: - return await self._base_edit( - name=name, - position=position, - topic=topic, - nsfw=nsfw, - rate_limit_per_user=rate_limit_per_user, - permission_overwrites=[o.to_dict() for o in permission_overwrites] - if permission_overwrites - else MISSING, - parent_id=parent_id, - default_auto_archive_duration=default_auto_archive_duration, - flags=flags.value if flags else MISSING, - available_tags=[t.to_dict() for t in available_tags] - if available_tags - else MISSING, - ) + def __init__(self, data: ThreadMemberData, state: State) -> None: + self._state: State = state + self.id: Maybe[int] = int(tid) if (tid := data.get("id")) else MISSING + self.user_id: Maybe[int] = int(uid) if (uid := data.get("user_id")) else MISSING + self.join_timestamp: datetime = datetime.fromisoformat(data["join_timestamp"]) + self.flags: int = data["flags"] + self.member: Maybe[GuildMember] = GuildMember(member, state) if (member := data.get("member")) else MISSING + async def remove(self) -> None: + return await self._state.http.remove_thread_member(self.id, self.user_id) -CHANNEL_TYPE = ( - TextChannel - | DMChannel - | VoiceChannel - | GroupDMChannel - | CategoryChannel - | AnnouncementChannel - | AnnouncementThread - | Thread - | StageChannel - | DirectoryChannel - | ForumChannel - | Channel -) +class StageChannel(VoiceChannel): + """ + Represents a guild stage channel. + + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: List[:class:`PermissionOverwrite`] + The channel's permission overwrites. + bitrate: :class:`int` + The channel's bitrate. + user_limit: :class:`int` + The channel's user limit. + rtc_region: Optional[:class:`str`] + The channel's RTC region. + video_quality_mode: Optional[:class:`VideoQualityMode`] + The channel's video quality mode. + parent_id: Optional[:class:`int`] + The ID of the category this channel belongs to. + nsfw: :class:`bool` + Whether the channel is NSFW. + last_message_id: Optional[:class:`int`] + The ID of the last message sent in the channel. + """ + type: ChannelType.GUILD_STAGE_VOICE -def identify_channel(data: dict[str, Any], state: State) -> CHANNEL_TYPE: - type = data['type'] - - if type == 0: - return TextChannel(data, state) - elif type == 1: - return DMChannel(data, state) - elif type == 2: - return VoiceChannel(data, state) - elif type == 4: - return CategoryChannel(data, state) - elif type == 5: - return AnnouncementChannel(data, state) - elif type == 10: - return AnnouncementThread(data, state) - elif type in (11, 12): - return Thread(data, state) - elif type == 13: - return StageChannel(data, state) - elif type == 14: - return DirectoryChannel(data, state) - elif type == 15: - return ForumChannel(data, state) - else: - return Channel(data, state) + +class ForumChannel(ThreadBasedChannel): + """ + Represents a guild forum channel. + + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: list[:class:`PermissionOverwrite`] + The channel's permission overwrites. + topic: :class:`str` | None + The channel's topic. + nsfw: :class:`bool` + Whether the channel is NSFW. + last_message_id: :class:`int` | None + The ID of the last message sent in the channel. + rate_limit_per_user: :class:`int` + The rate limit per user in seconds. + available_tags: list[:class:`ForumTag`] + The available tags for the channel. + default_reaction_emoji: :class:`DefaultReaction` | None + The default reaction emoji for the channel. + default_thread_rate_limit_per_user: :class:`int` + The default thread rate limit per user in seconds. + default_sort_order: :class:`SortOrderType` | None + The default sort order for the channel. + default_forum_layout: :class:`ForumLayoutType` + The default forum layout for the channel. + """ + type: ChannelType.GUILD_FORUM + + +class MediaChannel(ThreadBasedChannel): + """ + Represents a guild media channel. + + Attributes + ----------- + type: :class:`ChannelType` + The channel's type. + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + position: :class:`int` + The channel's position. + permission_overwrites: list[:class:`PermissionOverwrite`] + The channel's permission overwrites. + topic: :class:`str` | None + The channel's topic. + nsfw: :class:`bool` + Whether the channel is NSFW. + last_message_id: :class:`int` | None + The ID of the last message sent in the channel. + rate_limit_per_user: :class:`int` + The rate limit per user in seconds. + available_tags: list[:class:`ForumTag`] + The available tags for the channel. + default_reaction_emoji: :class:`DefaultReaction` | None + The default reaction emoji for the channel. + default_thread_rate_limit_per_user: :class:`int` + The default thread rate limit per user in seconds. + default_sort_order: :class:`SortOrderType` | None + The default sort order for the channel. + default_forum_layout: :class:`ForumLayoutType` + The default forum layout for the channel. + """ + type: ChannelType.GUILD_MEDIA + + +Channel = Union[ + TextChannel, + DMChannel, + VoiceChannel, + GroupDMChannel, + CategoryChannel, + NewsChannel, + Thread, + StageChannel, + ForumChannel, + MediaChannel, +] +channel_types: dict[ChannelType, type[Channel]] = { + ChannelType.GUILD_TEXT: TextChannel, + ChannelType.DM: DMChannel, + ChannelType.GUILD_VOICE: VoiceChannel, + ChannelType.GROUP_DM: GroupDMChannel, + ChannelType.GUILD_CATEGORY: CategoryChannel, + ChannelType.GUILD_ANNOUNCEMENT: NewsChannel, + ChannelType.ANNOUNCEMENT_THREAD: Thread, + ChannelType.PUBLIC_THREAD: Thread, + ChannelType.PRIVATE_THREAD: Thread, + ChannelType.GUILD_STAGE_VOICE: StageChannel, + ChannelType.GUILD_FORUM: ForumChannel, + ChannelType.GUILD_MEDIA: MediaChannel, +} + + +def channel_factory(data: ChannelData | PartialChannelData, state: State) -> Channel: + return channel_types[ChannelType(data["type"])](data, state) diff --git a/pycord/color.py b/pycord/color.py index 389d08d8..b357d962 100644 --- a/pycord/color.py +++ b/pycord/color.py @@ -19,139 +19,147 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE +from __future__ import annotations + +from typing import Self + class Color: - """Represents the default discord colors. - Defines factory methods which return a certain color code to be used. + """High-level representation of Discord accent colors. + + Parameters + ---------- + value: :class:`int` + An integer value for this color. """ def __init__(self, value: int): if not isinstance(value, int): - raise TypeError('Expected a integer.') + raise TypeError("Expected a integer.") self.value: int = value @classmethod - def default(cls) -> 'Color': + def default(cls) -> Self: """A factory color method which returns `0`""" return cls(0) @classmethod - def teal(cls) -> 'Color': + def teal(cls) -> Self: """A factory color method which returns `0x1ABC9C`""" return cls(0x1ABC9C) @classmethod - def dark_teal(cls) -> 'Color': + def dark_teal(cls) -> Self: """A factory color method which returns `0x11806A`""" return cls(0x11806A) @classmethod - def brand_green(cls) -> 'Color': + def brand_green(cls) -> Self: """A factory color method which returns `0x57F287`""" return cls(0x57F287) @classmethod - def green(cls) -> 'Color': + def green(cls) -> Self: """A factory color method which returns `0x2ECC71`""" return cls(0x2ECC71) @classmethod - def dark_green(cls) -> 'Color': + def dark_green(cls) -> Self: """A factory color method which returns `0x1F8B4C`""" return cls(0x1F8B4C) @classmethod - def blue(cls) -> 'Color': + def blue(cls) -> Self: """A factory color method which returns `0x3498DB`""" return cls(0x3498DB) @classmethod - def dark_blue(cls) -> 'Color': + def dark_blue(cls) -> Self: """A factory color method which returns `0x206694`""" return cls(0x206694) @classmethod - def purple(cls) -> 'Color': + def purple(cls) -> Self: """A factory color method which returns `0x9b59b6`""" return cls(0x9B59B6) @classmethod - def dark_purple(cls) -> 'Color': + def dark_purple(cls) -> Self: """A factory color method which returns `0x71368A`""" return cls(0x71368A) @classmethod - def magenta(cls) -> 'Color': + def magenta(cls) -> Self: """A factory color method which returns `0xE91E63`""" return cls(0xE91E63) @classmethod - def dark_magenta(cls) -> 'Color': + def dark_magenta(cls) -> Self: """A factory color method which returns `0xAD1457`""" return cls(0xAD1457) @classmethod - def gold(cls) -> 'Color': + def gold(cls) -> Self: """A factory color method which returns `0xF1C40F`""" return cls(0xF1C40F) @classmethod - def dark_gold(cls) -> 'Color': + def dark_gold(cls) -> Self: """A factory color method which returns `0xC27C0E`""" return cls(0xC27C0E) @classmethod - def orange(cls) -> 'Color': + def orange(cls) -> Self: """A factory color method which returns `0xE67E22`""" return cls(0xE67E22) @classmethod - def dark_orange(cls) -> 'Color': + def dark_orange(cls) -> Self: """A factory color method which returns `0xA84300`""" return cls(0xA84300) @classmethod - def brand_red(cls) -> 'Color': + def brand_red(cls) -> Self: """A factory color method which returns `0xED4245`""" return cls(0xED4245) @classmethod - def red(cls) -> 'Color': + def red(cls) -> Self: """A factory color method which returns `0xE74C3C`""" return cls(0xE74C3C) @classmethod - def dark_red(cls) -> 'Color': + def dark_red(cls) -> Self: """A factory color method which returns `0x992D22`""" return cls(0x992D22) @classmethod - def dark_gray(cls) -> 'Color': + def dark_gray(cls) -> Self: """A factory color method which returns `0x607D8B`""" return cls(0x607D8B) @classmethod - def light_gray(cls) -> 'Color': + def light_gray(cls) -> Self: """A factory color method which returns `0x979C9F`""" return cls(0x979C9F) @classmethod - def blurple(cls) -> 'Color': + def blurple(cls) -> Self: """A factory color method which returns `0x5865F2`""" return cls(0x5865F2) @classmethod - def dark_theme(cls) -> 'Color': + def dark_theme(cls) -> Self: """A factory color method which returns `0x2F3136`""" return cls(0x2F3136) @classmethod - def fushia(cls) -> 'Color': + def fushia(cls) -> Self: """A factory color method which returns `0xEB459E`""" return cls(0xEB459E) @classmethod - def yellow(cls) -> 'Color': + def yellow(cls) -> Self: """A factory color method which returns `0xFEE75C`""" return cls(0xFEE75C) diff --git a/pycord/commands/__init__.py b/pycord/commands/__init__.py deleted file mode 100644 index 783e61c1..00000000 --- a/pycord/commands/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -pycord.commands -~~~~~~~~~~~~~~~ -Flexible and Versatile command system for Pycord. - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .application import * -from .command import * -from .group import * diff --git a/pycord/commands/application/__init__.py b/pycord/commands/application/__init__.py deleted file mode 100644 index 4c687f7e..00000000 --- a/pycord/commands/application/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -pycord.commands.application -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Implementation of Application Commands - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .command import * -from .context import * -from .errors import * diff --git a/pycord/commands/application/command.py b/pycord/commands/application/command.py deleted file mode 100644 index 463dd0c1..00000000 --- a/pycord/commands/application/command.py +++ /dev/null @@ -1,769 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -import asyncio -from copy import copy -from inspect import isclass -from typing import TYPE_CHECKING, Any, Sequence, Union, get_origin - -from ...channel import Channel, identify_channel -from ...enums import ApplicationCommandOptionType, ApplicationCommandType -from ...events.other import InteractionCreate -from ...interaction import Interaction, InteractionOption -from ...media import Attachment -from ...member import Member -from ...message import Message -from ...missing import MISSING, Maybe, MissingEnum -from ...role import Role -from ...snowflake import Snowflake -from ...types import AsyncFunc -from ...types.interaction import ApplicationCommandData -from ...user import User -from ...utils import get_arg_defaults, get_args, remove_undefined -from ..command import Command -from ..group import Group -from .context import Context -from .errors import ApplicationCommandException - -if TYPE_CHECKING: - from ...state import State - -__all__: Sequence[str] = ('CommandChoice', 'Option', 'ApplicationCommand') - - -async def _autocomplete( - _: Interaction, option: Option, string: str -) -> list[dict[str, Any]]: - string = str(string).lower() - return [ - choice._to_dict() - for choice in option._choices - if string in str(choice.value).lower() - ] - - -class CommandChoice: - """ - A single choice of an option. Used often in autocomplete-based commands - - Parameters - ---------- - name: :class:`str` - The name of this choice - value: :class:`str` | :class:`int` :class:`float` - The value of this choice - name_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - """ - - def __init__( - self, - name: str, - name_localizations: dict[str, str] | None = None, - value: str | int | float | None = None, - ) -> None: - self.name = name - - if value is None: - self.value = name - - self.name_localizations = name_localizations - - def _to_dict(self) -> dict[str, Any]: - return { - 'name': self.name, - 'value': self.value, - 'name_localizations': self.name_localizations, - } - - -_OPTION_BIND = { - str: ApplicationCommandOptionType.STRING, - int: ApplicationCommandOptionType.INTEGER, - bool: ApplicationCommandOptionType.BOOLEAN, - float: ApplicationCommandOptionType.NUMBER, - User: ApplicationCommandOptionType.USER, - Channel: ApplicationCommandOptionType.CHANNEL, - Role: ApplicationCommandOptionType.ROLE, - Attachment: ApplicationCommandOptionType.ATTACHMENT, -} - - -class Option: - """ - An option of a Chat Input Command. - - Parameters - ---------- - type: :class:`.ApplicationCommandOptionType` | :class:`int` - The type of Option - name: :class:`str` - The name of this Option - description: :class:`str` - The description of what and why this option is needed - name_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - description_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - required: bool - Is this option required to pass - Defaults to False. - choices: list[:class:`.CommandChoice`] - The choices this option can be given. Often used in autocomplete - options: list[:class:`Option`] - Extra Options to add. ONLY support for Sub Commands - channel_types: list[:class:`int`] - A list of channel types to keep lock of - min_value: :class:`int` - The minimum integer value - max_value: :class:`int` - The maximum integer value - autocomplete: :class:`bool` - Whether to implement autocomplete or not - """ - - _level: int = 0 - - def __init__( - self, - name: str | None = None, - description: str | None = None, - type: ApplicationCommandOptionType - | int - | Any = ApplicationCommandOptionType.STRING, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - required: bool | MissingEnum = MISSING, - choices: list[CommandChoice] | MissingEnum = MISSING, - options: list['Option'] | MissingEnum = MISSING, - channel_types: list[int] | MissingEnum = MISSING, - min_value: int | MissingEnum = MISSING, - max_value: int | MissingEnum = MISSING, - autocomplete: bool | MissingEnum = MISSING, - autocompleter: AsyncFunc = _autocomplete, - ) -> None: - if isinstance(type, ApplicationCommandOptionType): - self.type = type.value - elif not isinstance(type, int): - self.type = _OPTION_BIND[type].value - else: - self.type = type - self.autocompleter = autocompleter - self.name = name - self.name_localizations = name_localizations - self.description = description or 'No description provided' - self.description_localizations = description_localizations - self.required = required - if autocomplete: - self.choices = [] - if choices: - self._choices = [choice for choice in choices if choices] - else: - self._choices = [] - else: - if choices: - self.choices = [choice._to_dict() for choice in choices] - else: - self.choices = MISSING - if options is MISSING: - self.options = [] - else: - self.options = options - self.channel_types = channel_types - self.min_value = min_value - self.max_value = max_value - self.autocomplete = autocomplete - self._subs = {} - - if TYPE_CHECKING: - self.focused: bool | MissingEnum = MISSING - self.value: str | int | float | MissingEnum = MISSING - self.options: list[InteractionOption] = MISSING - self._param: str = MISSING - self._callback: AsyncFunc | None = MISSING - - @property - def callback(self) -> AsyncFunc: - return self._callback - - @callback.setter - def callback(self, call: AsyncFunc | None) -> None: - if call is None: - self._callback = None - return - - arg_defaults = get_arg_defaults(self._callback) - self.options: list[Option] = [] - - i: int = 0 - - for name, v in arg_defaults.items(): - # ignore interaction - if i == 0: - i += 1 - continue - - if v[0] is None and name != 'self': - raise ApplicationCommandException( - f'Parameter {name} on sub command {self.name} has no default set' - ) - elif name == 'self': - continue - elif not isinstance(v[0], Option): - raise ApplicationCommandException( - f'Options may only be of type Option, not {v[0]}' - ) - - v[0]._param = name - - self.options.append(v[0]) - - def __get__(self): - return self.value - - def _inter_copy(self, data: InteractionOption) -> Option: - c = copy(self) - - c.focused = data.focused - c.value = data.value - c.options = data.options - return c - - def to_dict(self) -> dict[str, Any]: - return remove_undefined( - type=self.type, - name=self.name, - name_localizations=self.name_localizations, - description=self.description, - description_localizations=self.description_localizations, - required=self.required, - choices=self.choices, - options=[option.to_dict() for option in self.options] - if self.options is not MISSING - else MISSING, - channel_types=self.channel_types, - min_value=self.min_value, - max_value=self.max_value, - autocomplete=self.autocomplete, - ) - - def command( - self, - name: str | None = None, - description: str | None = None, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - ) -> ApplicationCommand: - """ - Add a command to this sub command to make it a group. - - Parameters - ---------- - name: :class:`str` - The name of this newly instantiated sub command - description: :class:`str` - The description of this newly created sub command - name_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - description_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - """ - - def wrapper(func: AsyncFunc): - command = Option( - type=1, - name=name or func.__name__.lower(), - description=description or func.__doc__ or 'No description provided', - name_localizations=name_localizations, - description_localizations=description_localizations, - ) - command._callback = func - - if self._level == 2: - raise ApplicationCommandException( - 'Sub commands cannot be three levels deep' - ) - - command._level = self._level + 1 - - if self._subs == {}: - self.options = [] - self._callback = None - - self.options.append(command) - - self._subs[name] = command - - if self.type == 1: - # turn into a command group - self.type = 2 - - return command - - return wrapper - - -class ApplicationCommand(Command): - """ - Commands deployed to Discord by Applications - - Parameters - ---------- - name: :class:`str` - The name of this Command - type: :class:`ApplicationCommandType` - The type of Application Command - description: :class:`str` - The description for this command - guild_id: :class:`int` - The Guild ID to limit this command to. - Defaults to None. - name_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - description_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - dm_permission: :class:`bool` - If this command should be instantiatable in DMs - Defaults to True. - nsfw: :class:`bool` - Whether this Application Command is for NSFW audiences or not - Defaults to False. - default_member_permissions: :class:`int` - The default member permissions for this command. - """ - - _processor_event = InteractionCreate(Context) - sub_level: int = 0 - - def __init__( - self, - # normal parameters - callback: AsyncFunc | None, - state: State, - name: str | MissingEnum = MISSING, - type: int | ApplicationCommandType = ApplicationCommandType.CHAT_INPUT, - description: str | MissingEnum = MISSING, - guild_id: int | None = None, - group: Group | None = None, - # discord parameters - name_localizations: dict[str, str] | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - dm_permission: bool | MissingEnum = MISSING, - nsfw: bool | MissingEnum = MISSING, - default_member_permissions: Maybe[int] = MISSING, - ) -> None: - super().__init__(callback, name, state, group) - - self.name = name or callback.__name__.lower() - - if isinstance(type, ApplicationCommandType): - self.type = type.value - else: - self.type = type - - self.guild_id = guild_id - self.name_localizations = name_localizations - if self.type == ApplicationCommandType.CHAT_INPUT.value: - self.description = ( - description or callback.__doc__ or 'No description provided' - ) - else: - self.description = MISSING - self.description_localizations = description_localizations - self.dm_permission = dm_permission - self.nsfw = nsfw - self._options = [] - self._parse_arguments() - if self.type == 1: - self._subs: dict[str, ApplicationCommand] = {} - self._created: bool = False - self.default_member_permissions = default_member_permissions - - def command( - self, - name: str | MissingEnum = MISSING, - description: str | MissingEnum = MISSING, - name_localizations: dict[str, str] | MissingEnum = MISSING, - description_localizations: dict[str, str] | MissingEnum = MISSING, - ) -> ApplicationCommand: - """ - Add a sub command to this command - - Parameters - ---------- - name: :class:`str` - The name of this newly instantiated sub command - description: :class:`str` - The description of this newly created sub command - name_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - description_localizations: dict[:class:`str`, :class:`str`] - Dictionary of localizations - """ - - def wrapper(func: AsyncFunc): - if self.type != 1: - raise ApplicationCommandException( - 'Sub Commands cannot be created on non-slash-commands' - ) - - command = Option( - type=1, - name=name or func.__name__.lower(), - description=description or func.__doc__ or 'No description provided', - name_localizations=name_localizations, - description_localizations=description_localizations, - ) - command._callback = func - command._level = 1 - - if self._subs == {}: - self.options = [] - self._options_dict = {} - self._options = [] - self._callback = None - - self.options.append(command) - self._options_dict[name] = command - self._options.append(command.to_dict()) - - self._subs[name] = command - return command - - return wrapper - - def _parse_user_command_arguments(self) -> None: - arg_defaults = get_arg_defaults(self._callback) - - fielded: bool = False - i: int = 0 - - for name, arg in arg_defaults.items(): - if i == 2: - raise ApplicationCommandException( - 'User Command has too many arguments, only one is allowed' - ) - - i += 1 - - if ( - arg[1] != Member - and arg[1] != User - and arg[1] != Interaction - and arg[1] != Union[User, Member] - ): - raise ApplicationCommandException( - 'Command argument incorrectly type hinted' - ) - - fielded = True - - self._user_command_field = name - - if not fielded: - raise ApplicationCommandException('No argument set for a member/user') - - def _parse_message_command_arguments(self) -> None: - arg_defaults = get_arg_defaults(self._callback) - - fielded: bool = False - i: int = 0 - - for _, arg in arg_defaults.items(): - if i == 3: - raise ApplicationCommandException( - 'Message Command has too many arguments, only two are allowed' - ) - - i += 1 - - if ( - arg[1] != Message - and arg[1] != Interaction - and arg[1] != User - and arg[1] != Member - and arg[1] != Union[User, Member] - ): - raise ApplicationCommandException( - 'Command argument incorrectly type hinted' - ) - - if arg[1] != Interaction: - fielded = True - - if not fielded: - raise ApplicationCommandException('No argument set for a message and user') - - def _parse_arguments(self) -> None: - if self.type == 2: - self._parse_user_command_arguments() - return - elif self.type == 3: - self._parse_message_command_arguments() - return - - arg_defaults = get_arg_defaults(self._callback) - self.options: list[Option] = [] - self._options_dict: dict[str, Option] = {} - - for name, v in arg_defaults.items(): - if name == 'self': - continue - elif v[1] is Interaction or v[1] is Context: - continue - - args = get_args(v[1]) - - if not isinstance(args[1], Option): - raise ApplicationCommandException( - f'Options may only be of type Option, not {args[1]}' - ) - - option: Option = args[1] - - option._param = name - option.type = _OPTION_BIND[args[0]].value - - if not option.name: - option.name = name - - self.options.append(option) - self._options_dict[option.name] = option - - for option in self.options: - self._options.append(option.to_dict()) - - async def instantiate(self) -> None: - if self.guild_id: - guild_commands: list[ - ApplicationCommandData - ] = await self._state.http.get_guild_application_commands( - self._state.user.id, self.guild_id, True - ) - - for app_cmd in guild_commands: - if app_cmd['name'] not in self._state._application_command_names: - await self._state.http.delete_guild_application_command( - self._state.user.id, self.guild_id, app_cmd['id'] - ) - continue - - if app_cmd['name'] == self.name and self._state.update_commands: - if app_cmd['type'] != self.type: - continue - - if self._created is True: - await self._state.http.delete_guild_application_command( - self._state.user.id, self.guild_id, app_cmd['id'] - ) - continue - - self.id = app_cmd['id'] - - await self._state.http.edit_guild_application_command( - self._state.user.id, - Snowflake(app_cmd['id']), - guild_id=self.guild_id, - name=self.name, - name_localizations=self.name_localizations, - description=self.description, - description_localizations=self.description_localizations, - type=self.type, - options=self._options, - ) - self._created = True - - if not self._created: - res = await self._state.http.create_guild_application_command( - self._state.user.id, - guild_id=self.guild_id, - name=self.name, - name_localizations=self.name_localizations, - description=self.description, - description_localizations=self.description_localizations, - type=self.type, - options=self._options, - ) - self.id = res['id'] - - return - - for app_cmd in self._state.application_commands: - if app_cmd['name'] == self.name and self._state.update_commands: - if app_cmd['type'] != self.type: - continue - - if self._created is True: - await self._state.http.delete_global_application_command( - self._state.user.id, app_cmd['id'] - ) - - await self._state.http.edit_global_application_command( - self._state.user.id, - Snowflake(app_cmd['id']), - name=self.name, - name_localizations=self.name_localizations, - description=self.description, - description_localizations=self.description_localizations, - type=self.type, - options=self._options, - ) - self._created = True - self.id = app_cmd['id'] - - if not self._created: - res = await self._state.http.create_global_application_command( - self._state.user.id, - name=self.name, - name_localizations=self.name_localizations, - description=self.description, - description_localizations=self.description_localizations, - type=self.type, - options=self._options, - ) - self.id = res['id'] - - async def _process_options( - self, - interaction: Interaction, - options: list[InteractionOption], - grouped: bool = False, - ) -> dict[str, Any]: - binding = {} - for option in options: - o = self._options_dict[option.name] - if option.type == 1: - sub = self._subs[option.name] - - opts = await self._process_options( - interaction=interaction, options=option.options - ) - if not grouped: - await self._callback(interaction) - await sub._callback(interaction, **opts) - elif option.type == 2: - await self._callback(interaction) - await self._process_options( - interaction=interaction, options=option.options, grouped=True - ) - elif option.type in (3, 4, 5, 10): - binding[o._param] = o._inter_copy(option).value - elif option.type == 6: - user = User( - interaction.data['resolved']['users'][option.value], self._state - ) - - if interaction.guild_id: - member = Member( - interaction.data['resolved']['members'][option.value], - self._state, - guild_id=interaction.guild_id, - ) - member.user = user - - binding[o._param] = member - else: - binding[o._param] = user - elif option.type == 7: - binding[o._param] = identify_channel( - interaction.data['resolved']['channels'][option.value], self._state - ) - elif option.type == 8: - binding[o._param] = Role( - interaction.data['resolved']['roles'][option.value], self._state - ) - elif option.type == 9: - if interaction.data['resolved'].get('roles'): - binding[o._param] = Role( - interaction.data['resolved']['roles'][option.value], self._state - ) - else: - user = User( - interaction.data['resolved']['users'][option.value], self._state - ) - - if interaction.guild_id: - member = Member( - interaction.data['resolved']['members'][option.value], - self._state, - ) - member.user = user - - binding[o._param] = member - else: - binding[o._param] = user - elif option.type == 11: - binding[o._param] = Attachment( - interaction.data['resolved']['attachments'][option.value], - self._state, - ) - - return binding - - def _process_user_command(self, inter: Interaction) -> User | Member: - if inter.member: - return inter.member - else: - return inter.user - - async def _invoke(self, event: InteractionCreate | Interaction) -> None: - if not isinstance(event, Interaction): - interaction = event.interaction - else: - interaction = event - - if interaction.type == 4: - if interaction.data.get('name') is not None: - if interaction.data['name'] == self.name: - option = interaction.data['options'][0] - real_option = self._options_dict[option['name']] - choices = await real_option.autocompleter( - interaction, real_option, option['value'] - ) - - await interaction.response.autocomplete(choices) - return - - if interaction.data: - if interaction.data.get('name') is not None: - if interaction.data['name'] == self.name: - if interaction.data['type'] == 1: - binding = await self._process_options( - interaction, interaction.options - ) - - if self._callback: - await self._callback(interaction, **binding) - elif interaction.data['type'] == 2: - user_binding = self._process_user_command(interaction) - - await self._callback(interaction, user_binding) - elif interaction.data['type'] == 3: - message = Message( - interaction.data['resolved']['messages'][ - interaction.data['target_id'] - ], - self._state, - ) - requester = self._process_user_command(interaction) - - await self._callback(interaction, message, requester) diff --git a/pycord/commands/application/context.py b/pycord/commands/application/context.py deleted file mode 100644 index ec88c3ee..00000000 --- a/pycord/commands/application/context.py +++ /dev/null @@ -1,136 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from ...embed import Embed -from ...flags import MessageFlags -from ...interaction import Interaction -from ...missing import MISSING, MissingEnum -from ...utils import remove_undefined - -if TYPE_CHECKING: - from ...state import State - from ...types import Interaction as InteractionData - - -class Context(Interaction): - """ - Contextual prelude to Interactions. - """ - - def __init__(self, data: InteractionData, state: State, **kwargs) -> None: - super().__init__(data, state, **kwargs) - - async def defer(self) -> None: - """ - Defer this interaction. Only defers if not deferred. - """ - - if not self.response._deferred: - await self.response.defer() - - async def send( - self, - content: str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - flags: int | MessageFlags | MissingEnum = MISSING, - ephemeral: bool | MissingEnum = MISSING, - ) -> None: - """ - Respond to an interaction. - - **Can only send once, if more needs to be done, use .send** - - Parameters - ---------- - content: :class:`str` - The message content to send. - tts: :class:`bool` - Whether TTS should be enabled for this message. - embeds: :class:`list`[:class:`.Embed`] - The embeds to send in this response. - flags: :class:`int` | :class:`.MessageFlags` - The flags to include in this message. - ephemeral: :class:`bool` - Whether to ephermalize this message or not. - """ - - if self.response.responded: - if ephemeral: - if not isinstance(flags, MessageFlags): - if not flags: - flags = MessageFlags() - else: - flags = MessageFlags.from_value(flags) - - flags.ephemeral = True - - await self.response.followup.send( - **remove_undefined(content=content, tts=tts, embeds=embeds, flags=flags) - ) - else: - await self.respond( - **remove_undefined(content=content, tts=tts, embeds=embeds, flags=flags) - ) - - async def respond( - self, - content: str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - flags: int | MessageFlags | MissingEnum = MISSING, - ephemeral: bool | MissingEnum = MISSING, - ) -> None: - """ - Respond to an interaction. - - **Can only send once, if more needs to be done, use .send** - - Parameters - ---------- - content: :class:`str` - The message content to send. - tts: :class:`bool` - Whether TTS should be enabled for this message. - embeds: :class:`list`[:class:`.Embed`] - The embeds to send in this response. - flags: :class:`int` | :class:`.MessageFlags` - The flags to include in this message. - ephemeral: :class:`bool` - Whether to ephermalize this message or not. - """ - - if ephemeral: - if not isinstance(flags, MessageFlags): - if not flags: - flags = MessageFlags() - else: - flags = MessageFlags.from_value(flags) - - flags.ephemeral = True - - await self.response.send( - **remove_undefined(content=content, tts=tts, embeds=embeds, flags=flags) - ) diff --git a/pycord/commands/command.py b/pycord/commands/command.py deleted file mode 100644 index 1636183d..00000000 --- a/pycord/commands/command.py +++ /dev/null @@ -1,49 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING, Type - -from ..events.event_manager import Event - -if TYPE_CHECKING: - from ..state import State - from ..types import AsyncFunc - from .group import Group - - -class Command: - _processor_event: Type[Event] | Event - - def __init__( - self, callback: AsyncFunc, name: str, state: State, group: Group | None = None - ) -> None: - self._callback = callback - - self.name = name - self.group = group - self._state = state - - async def instantiate(self) -> None: - ... - - async def _invoke(self, *args, **kwargs) -> None: - pass diff --git a/pycord/commands/group.py b/pycord/commands/group.py deleted file mode 100644 index 627bd189..00000000 --- a/pycord/commands/group.py +++ /dev/null @@ -1,65 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING, TypeVar - -from .command import Command - -if TYPE_CHECKING: - from ..state import State - from ..types import AsyncFunc - -T = TypeVar('T') - - -class Group: - def __init__(self, func: AsyncFunc | None, name: str, state: State) -> None: - self.commands: list[Command] = [] - # nested groups - self.groups: list['Group'] = [] - - self.name = name - self._callback = func - self.__state = state - self._pending_commands: list[Command] = [] - - @property - def _state(self) -> State: - return self.__state - - @_state.setter - def _state(self, state: State) -> None: - self.__state = state - - for command in self._pending_commands: - self._state.commands.append(command) - - def command(self, name: str) -> T: - def wrapper(func: T) -> T: - command = Command(func, name=name, state=self._state, group=self) - if self._state: - self._state.commands.append(command) - else: - self._pending_commands.append(command) - return func - - return wrapper diff --git a/pycord/connection.py b/pycord/connection.py deleted file mode 100644 index 59efe6b7..00000000 --- a/pycord/connection.py +++ /dev/null @@ -1,52 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .enums import VisibilityType -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import ( - SERVICE, - Connection as DiscordConnection, - Integration as DiscordIntegration, -) - -if TYPE_CHECKING: - from .state import State - - -class Connection: - def __init__(self, data: DiscordConnection, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.type: SERVICE = data['type'] - self.revoked: bool | MissingEnum = data.get('revoked', MISSING) - self._integrations: list[DiscordIntegration] | MissingEnum = data.get( - 'integrations', MISSING - ) - self.verified: bool = data['verified'] - self.friend_sync: bool = data['friend_sync'] - self.show_activity: bool = data['show_activity'] - self.two_way_linked: bool = data['two_way_link'] - self.visibility: VisibilityType = VisibilityType(data['visibility']) diff --git a/pycord/commands/application/errors.py b/pycord/custom_types.py similarity index 85% rename from pycord/commands/application/errors.py rename to pycord/custom_types.py index d6e63b0b..250552f7 100644 --- a/pycord/commands/application/errors.py +++ b/pycord/custom_types.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,10 +18,10 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. -from ...errors import PycordException +from typing import Any, Callable, Coroutine, TypeVar -class ApplicationCommandException(PycordException): - ... +T = TypeVar("T") +AsyncFunc = Callable[..., Coroutine[Any, Any, T]] diff --git a/pycord/embed.py b/pycord/embed.py index 579d9b5c..158d0af4 100644 --- a/pycord/embed.py +++ b/pycord/embed.py @@ -1,247 +1,343 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from datetime import datetime -from typing import Any - -from .color import Color -from .missing import MISSING, Maybe, MissingEnum -from .types import ( - Author as DiscordAuthor, - Embed as DiscordEmbed, - Field as DiscordField, - Footer as DiscordFooter, - Image as DiscordImage, - Provider as DiscordProvider, - Thumbnail as DiscordThumbnail, - Video as DiscordVideo, -) -from .utils import remove_undefined +from __future__ import annotations +from discord_typings._resources._channel import ( + EmbedAuthorData, EmbedData, EmbedFieldData, EmbedFooterData, EmbedImageData, EmbedThumbnailData, + EmbedVideoData, +) -# pure data classes, no user interaction. -class Provider: - def __init__(self, data: DiscordProvider) -> None: - self.name: MissingEnum | str = data.get('name', MISSING) - self.url: MissingEnum | str = data.get('url', MISSING) +from pycord import remove_undefined +from pycord.missing import Maybe, MISSING -class Video: - def __init__(self, data: DiscordVideo) -> None: - self.url: MissingEnum | str = data.get('url', MISSING) - self.proxy_url: MissingEnum | str = data.get('proxy_url', MISSING) - self.width: MissingEnum | int = data.get('width', MISSING) - self.height: MissingEnum | int = data.get('height', MISSING) +class Embed: + def __init__( + self, + title: Maybe[str] = MISSING, + type: Maybe[str] = MISSING, + description: Maybe[str] = MISSING, + url: Maybe[str] = MISSING, + timestamp: Maybe[str] = MISSING, + color: Maybe[int] = MISSING, + footer: Maybe[EmbedFooter] = MISSING, + image: Maybe[EmbedImage] = MISSING, + thumbnail: Maybe[EmbedThumbnail] = MISSING, + video: Maybe[EmbedVideo] = MISSING, + provider: Maybe[EmbedProvider] = MISSING, + author: Maybe[EmbedAuthor] = MISSING, + fields: Maybe[list[EmbedField]] = MISSING, + ): + self.title: Maybe[str] = title + self.type: Maybe[str] = type + self.description: Maybe[str] = description + self.url: Maybe[str] = url + self.timestamp: Maybe[str] = timestamp + self.color: Maybe[int] = color + self.footer: Maybe[EmbedFooter] = footer + self.image: Maybe[EmbedImage] = image + self.thumbnail: Maybe[EmbedThumbnail] = thumbnail + self.video: Maybe[EmbedVideo] = video + self.provider: Maybe[EmbedProvider] = provider + self.author: Maybe[EmbedAuthor] = author + self.fields: Maybe[list[EmbedField]] = fields + def validate(self) -> None: + if self.title is not MISSING and len(self.title) > 256: + raise ValueError("Title cannot exceed 256 characters.") + if self.description is not MISSING and len(self.description) > 2048: + raise ValueError("Description cannot exceed 2048 characters.") + if self.fields is not MISSING and len(self.fields) > 25: + raise ValueError("Number of fields cannot exceed 25.") + if self.footer is not MISSING: + self.footer.validate() + if self.author is not MISSING: + self.author.validate() + if self.fields is not MISSING: + for field in self.fields: + field.validate() + if self.image is not MISSING: + self.image.validate() + if self.thumbnail is not MISSING: + self.thumbnail.validate() + if len(self) > 6000: + raise ValueError("Total length of embed cannot exceed 6000 characters.") -class Thumbnail: - def __init__(self, url: str) -> None: - self.url: str = url - self.proxy_url: str | MissingEnum = MISSING - self.height: MissingEnum | int = MISSING - self.width: MissingEnum | int = MISSING + def to_dict(self, *, to_send: bool = False) -> EmbedData: + if to_send: + return remove_undefined( + **{ + "title": self.title, + "type": self.type, + "description": self.description, + "url": self.url, + "timestamp": self.timestamp, + "color": self.color, + "footer": self.footer.to_dict(to_send=True) if self.footer is not MISSING else MISSING, + "image": self.image.to_dict(to_send=True) if self.image is not MISSING else MISSING, + "thumbnail": self.thumbnail.to_dict(to_send=True) if self.thumbnail is not MISSING else MISSING, + "author": self.author.to_dict(to_send=True) if self.author is not MISSING else MISSING, + "fields": [field.to_dict() for field in self.fields] if self.fields is not MISSING else MISSING + } + ) + return remove_undefined( + **{ + "title": self.title, + "type": self.type, + "description": self.description, + "url": self.url, + "timestamp": self.timestamp, + "color": self.color, + "footer": self.footer.to_dict() if self.footer is not MISSING else MISSING, + "image": self.image.to_dict() if self.image is not MISSING else MISSING, + "thumbnail": self.thumbnail.to_dict() if self.thumbnail is not MISSING else MISSING, + "video": self.video.to_dict() if self.video is not MISSING else MISSING, + "provider": self.provider.to_dict() if self.provider is not MISSING else MISSING, + "author": self.author.to_dict() if self.author is not MISSING else MISSING, + "fields": [field.to_dict() for field in self.fields] if self.fields is not MISSING else MISSING + } + ) @classmethod - def _from_data(cls, data: DiscordThumbnail) -> 'Thumbnail': - self = cls(data['url']) - self.proxy_url = data.get('proxy_url', MissingEnum) - self.height = data.get('height', MissingEnum) - self.width = data.get('width', MissingEnum) - return self + def from_dict(cls, data: EmbedData) -> Embed: + return cls( + title=data.get("title", MISSING), + type=data.get("type", MISSING), + description=data.get("description", MISSING), + url=data.get("url", MISSING), + timestamp=data.get("timestamp", MISSING), + color=data.get("color", MISSING), + footer=EmbedFooter.from_dict(data.get("footer", {})), + image=EmbedImage.from_dict(data.get("image", {})), + thumbnail=EmbedThumbnail.from_dict(data.get("thumbnail", {})), + video=EmbedVideo.from_dict(data.get("video", {})), + provider=EmbedProvider.from_dict(data.get("provider", {})), + author=EmbedAuthor.from_dict(data.get("author", {})), + fields=[EmbedField.from_dict(field) for field in data.get("fields", [])] + ) - def _to_data(self) -> dict[str, Any]: - return remove_undefined(url=self.url) + def __len__(self): + return len(self.title or "") + len(self.description or "") + sum(len(field) for field in self.fields or []) + \ + len(self.footer or "") + len(self.author or "") -class Image: - def __init__(self, url: str) -> None: - self.url: str = url - self.proxy_url: str | MissingEnum = MISSING - self.height: MissingEnum | int = MISSING - self.width: MissingEnum | int = MISSING +class EmbedFooter: + def __init__(self, *, text: str, icon_url: Maybe[str] = MISSING, proxy_icon_url: Maybe[str] = MISSING): + self.text: str = text + self.icon_url: Maybe[str] = icon_url + self.proxy_icon_url: Maybe[str] = proxy_icon_url + + def validate(self) -> None: + if len(self.text) > 2048: + raise ValueError("Footer text cannot exceed 2048 characters.") + if self.icon_url is not MISSING and not self.icon_url.startswith("https://") \ + and not self.icon_url.startswith("http://") and not self.icon_url.startswith("attachment://"): + raise ValueError("Footer icon url must have a protocol of http, https, or attachment.") + + def to_dict(self, *, to_send: bool = False) -> EmbedFooterData: + if to_send: + return remove_undefined(**{"text": self.text, "icon_url": self.icon_url}) + return remove_undefined( + **{ + "text": self.text, + "icon_url": self.icon_url, + "proxy_icon_url": self.proxy_icon_url + } + ) @classmethod - def _from_data(cls, data: DiscordImage) -> 'Image': - self = cls(data['url']) - self.proxy_url = data.get('proxy_url', MissingEnum) - self.height = data.get('height', MissingEnum) - self.width = data.get('width', MissingEnum) - return self + def from_dict(cls, data: EmbedFooterData) -> EmbedFooter: + return cls( + text=data["text"], + icon_url=data.get("icon_url", MISSING), + proxy_icon_url=data.get("proxy_icon_url", MISSING) + ) - def _to_data(self) -> dict[str, Any]: - return remove_undefined(url=self.url) + def __len__(self): + return len(self.text) -class Footer: - def __init__(self, text: str, icon_url: str | MissingEnum = MISSING) -> None: - self.text = text - self.icon_url = icon_url - self.proxy_icon_url: MissingEnum | str = MISSING +class EmbedImage: + def __init__( + self, *, url: str, proxy_url: Maybe[str] = MISSING, height: Maybe[int] = MISSING, + width: Maybe[int] = MISSING + ): + self.url: str = url + self.proxy_url: Maybe[str] = proxy_url + self.height: Maybe[int] = height + self.width: Maybe[int] = width - @classmethod - def _from_data(cls, data: DiscordFooter) -> 'Footer': - self = cls(data['text'], data.get('icon_url', MISSING)) - self.proxy_icon_url = data.get('proxy_icon_url', MISSING) - return self + def validate(self) -> None: + if not self.url.startswith("https://") and not self.url.startswith("http://") \ + and not self.url.startswith("attachment://"): + raise ValueError("Image url must have a protocol of http, https, or attachment.") - def _to_data(self) -> dict[str, Any]: - return remove_undefined(text=self.text, icon_url=self.icon_url) + def to_dict(self, *, to_send: bool = False) -> EmbedImageData: + if to_send: + return {"url": self.url} + return remove_undefined( + **{ + "url": self.url, + "proxy_url": self.proxy_url, + "height": self.height, + "width": self.width + } + ) + + @classmethod + def from_dict(cls, data: EmbedImageData) -> EmbedImage: + return cls( + url=data["url"], + proxy_url=data.get("proxy_url", MISSING), + height=data.get("height", MISSING), + width=data.get("width", MISSING) + ) -class Author: +class EmbedThumbnail: def __init__( - self, - name: str, - icon_url: str | MissingEnum = MISSING, - url: str | MissingEnum = MISSING, - ) -> None: - self.name = name - self.url = url - self.icon_url = icon_url - self.proxy_icon_url: MissingEnum | str = MISSING + self, *, url: str, proxy_url: Maybe[str] = MISSING, height: Maybe[int] = MISSING, width: Maybe[int] = MISSING + ): + self.url: str = url + self.proxy_url: Maybe[str] = proxy_url + self.height: Maybe[int] = height + self.width: Maybe[int] = width - @classmethod - def _from_data(cls, data: DiscordAuthor) -> 'Author': - self = cls(data['name'], data.get('icon_url', MISSING, data['url'])) - self.proxy_icon_url = data.get('proxy_icon_url', MISSING) - return self + def validate(self) -> None: + if self.url is not MISSING and not self.url.startswith("https://") \ + and not self.url.startswith("http://") and not self.url.startswith("attachment://"): + raise ValueError("Thumbnail url must have a protocol of http, https, or attachment.") - def _to_data(self) -> dict[str, Any]: - return remove_undefined(name=self.name, url=self.url, icon_url=self.icon_url) + def to_dict(self, *, to_send: bool = False) -> EmbedThumbnailData: + if to_send: + return {"url": self.url} + return remove_undefined( + **{ + "url": self.url, + "proxy_url": self.proxy_url, + "height": self.height, + "width": self.width + } + ) + + @classmethod + def from_dict(cls, data: EmbedThumbnailData) -> EmbedThumbnail: + return cls( + url=data["url"], + proxy_url=data.get("proxy_url", MISSING), + height=data.get("height", MISSING), + width=data.get("width", MISSING) + ) -class Field: +class EmbedVideo: def __init__( - self, name: str, value: str, inline: bool | MissingEnum = MISSING - ) -> None: - self.name = name - self.value = value - self.inline = inline + self, *, url: Maybe[str] = MISSING, proxy_url: Maybe[str] = MISSING, + height: Maybe[int] = MISSING, width: Maybe[int] = MISSING + ): + self.url: Maybe[str] = url + self.proxy_url: Maybe[str] = proxy_url + self.height: Maybe[int] = height + self.width: Maybe[int] = width + + def to_dict(self) -> EmbedVideoData: + return remove_undefined( + **{ + "url": self.url, + "proxy_url": self.proxy_url, + "height": self.height, + "width": self.width + } + ) @classmethod - def _from_data(cls, data: DiscordField) -> 'Field': - return cls(data['name'], data['value'], data.get('field', MISSING)) + def from_dict(cls, data: EmbedVideoData) -> EmbedVideo: + return cls( + url=data.get("url", MISSING), + proxy_url=data.get("proxy_url", MISSING), + height=data.get("height", MISSING), + width=data.get("width", MISSING) + ) - def _to_data(self) -> dict[str, Any]: - return remove_undefined(name=self.name, value=self.value, inline=self.inline) +class EmbedProvider: + def __init__(self, *, name: Maybe[str] = MISSING, url: Maybe[str] = MISSING): + self.name: Maybe[str] = name + self.url: Maybe[str] = url -# settable: -# footer -# fields -# image -# thumbnail -# author -# NOTE: this is the only class not having data being parsed in __init__ because of it being used mostly by users. -class Embed: - def __init__( - self, - *, - title: str, - description: str | MissingEnum = MISSING, - url: str | MissingEnum = MISSING, - timestamp: datetime | MissingEnum = MISSING, - color: Color | MissingEnum = MISSING, - thumbnail: Thumbnail | MissingEnum = MISSING, - author: Author | MissingEnum = MISSING, - footer: Footer | MissingEnum = MISSING, - image: Image | MissingEnum = MISSING, - fields: list[Field] = None, - ) -> None: - if fields is None: - fields = [] - self.thumbnail = thumbnail - self.author = author - self.footer = footer - self.image = image - self.fields = fields - self.title = title - self.description = description - self.url = url - self.timestamp = timestamp - self.color = color + def to_dict(self) -> dict[str, str]: + return remove_undefined(**{"name": self.name, "url": self.url}) @classmethod - def _from_data(cls, data: DiscordEmbed) -> None: - color = Color(data.get('color')) if data.get('color') is not None else None - thumbnail = ( - Thumbnail._from_data(data.get('thumbnail')) - if data.get('thumbnail') is not None - else None - ) - video = Video(data.get('video')) if data.get('video') is not None else None - provider = ( - Provider(data.get('provider')) if data.get('provider') is not None else None - ) - author = ( - Author._from_data(data.get('author')) - if data.get('author') is not None - else None + def from_dict(cls, data: dict[str, str]) -> EmbedProvider: + return cls( + name=data.get("name", MISSING), + url=data.get("url", MISSING), ) - footer = ( - Footer._from_data(data.get('footer')) - if data.get('footer') is not None - else None - ) - image = ( - Image._from_data(data.get('thumbnail')) - if data.get('thumbnail') is not None - else None + + +class EmbedAuthor: + def __init__( + self, *, name: str, url: Maybe[str] = MISSING, icon_url: Maybe[str] = MISSING, + proxy_icon_url: Maybe[str] = MISSING + ): + self.name: str = name + self.url: Maybe[str] = url + self.icon_url: Maybe[str] = icon_url + self.proxy_icon_url: Maybe[str] = proxy_icon_url + + def validate(self) -> None: + if len(self.name) > 256: + raise ValueError("Author name cannot exceed 256 characters.") + if self.icon_url is not MISSING and not self.url.startswith("https://") \ + and not self.url.startswith("http://") and not self.url.startswith("attachment://"): + raise ValueError("Author icon url must have a protocol of http, https, or attachment.") + + def to_dict(self, *, to_send: bool = False) -> EmbedAuthorData: + if to_send: + return remove_undefined(**{"name": self.name, "url": self.url, "icon_url": self.icon_url}) + return remove_undefined( + **{ + "name": self.name, + "url": self.url, + "icon_url": self.icon_url, + "proxy_icon_url": self.proxy_icon_url + } ) - fields = [Field._from_data(field) for field in data.get('fields', [])] - - self = cls( - title=data.get('title'), - description=data.get('description'), - url=data.get('url'), - timestamp=datetime.fromisoformat(data.get('timestamp')) - if data.get('timestamp') is not None - else None, - color=color, - footer=footer, - image=image, - thumbnail=thumbnail, - author=author, - fields=fields, + + @classmethod + def from_dict(cls, data: EmbedAuthorData) -> EmbedAuthor: + return cls( + name=data["name"], + url=data.get("url", MISSING), + icon_url=data.get("icon_url", MISSING), + proxy_icon_url=data.get("proxy_icon_url", MISSING) ) - self.video = video - self.provider = provider - - def _to_data(self) -> dict[str, Any]: - color = self.color.value if self.color else None - thumbnail = self.thumbnail._to_data() if self.thumbnail else MISSING - author = self.author._to_data() if self.author else MISSING - footer = self.footer._to_data() if self.footer else MISSING - image = self.image._to_data() if self.image else MISSING - fields = [field._to_data() for field in self.fields] - timestamp = self.timestamp.isoformat() if self.timestamp else MISSING - return remove_undefined( - title=self.title, - description=self.description, - url=self.url, - color=color, - timestamp=timestamp, - thumbnail=thumbnail, - footer=footer, - author=author, - image=image, - fields=fields, + def __len__(self): + return len(self.name) + + +class EmbedField: + def __init__(self, *, name: str, value: str, inline: Maybe[bool] = MISSING): + self.name: str = name + self.value: str = value + self.inline: Maybe[bool] = inline + + def validate(self) -> None: + if len(self.name) > 256: + raise ValueError("Field name cannot exceed 256 characters.") + if len(self.value) > 1024: + raise ValueError("Field value cannot exceed 1024 characters.") + + def to_dict(self) -> EmbedFieldData: + return remove_undefined(**{"name": self.name, "value": self.value, "inline": self.inline}) + + @classmethod + def from_dict(cls, data: EmbedFieldData) -> EmbedField: + return cls( + name=data["name"], + value=data["value"], + inline=data.get("inline", MISSING) ) + + def __len__(self): + return len(self.name) + len(self.value) diff --git a/pycord/emoji.py b/pycord/emoji.py new file mode 100644 index 00000000..62cbabbc --- /dev/null +++ b/pycord/emoji.py @@ -0,0 +1,99 @@ +# cython: language_level=3 +# Copyright (c) 2022-present Pycord Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE + +from __future__ import annotations + +from .user import User +from .asset import Asset +from .mixins import Identifiable +from .missing import Maybe, MISSING + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from discord_typings import EmojiData + from .state import State + +__all__ = ( + "Emoji", +) + + +class Emoji(Identifiable): + """ + Represents a custom emoji. + + Attributes + ----------- + id: :class:`int` + The ID of the emoji. + name: :class:`str` + The name of the emoji. + roles: List[:class:`int`] + A list of roles that may use the emoji. + user: Optional[:class:`User`] + The user that created the emoji. + require_colons: Optional[:class:`bool`] + Whether the emoji requires colons to be used. + managed: Optional[:class:`bool`] + Whether the emoji is managed by an integration. + animated: Optional[:class:`bool`] + Whether the emoji is animated. + available: Optional[:class:`bool`] + Whether the emoji is available. + """ + __slots__ = ( + "_state", + "id", + "name", + "roles", + "user", + "require_colons", + "managed", + "animated", + "available", + ) + + def __init__(self, data: EmojiData, state: State) -> None: + self._state: State = state + self._update(data) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return f"<:{self.name}:{self.id}>" if not self.animated else f"" + + def _update(self, data: "EmojiData") -> None: + self.id: int | None = int(_id) if (_id := data.get("id")) else None + self.name: str | None = data["name"] + self.roles: list[int] = [int(r) for r in data.get("roles", [])] + self.user: Maybe[User] = User(user, self._state) if (user := data.get("user")) else MISSING + self.require_colons: Maybe[bool] = data.get("require_colons", MISSING) + self.managed: Maybe[bool] = data.get("managed", MISSING) + self.animated: Maybe[bool] = data.get("animated", MISSING) + self.available: Maybe[bool] = data.get("available", MISSING) + + @property + def asset(self) -> Asset | None: + if self.id is None: + return None + return Asset.from_custom_emoji(self._state, self.id, bool(self.animated)) diff --git a/pycord/enums.py b/pycord/enums.py index 6beee6e7..2b98aa4c 100644 --- a/pycord/enums.py +++ b/pycord/enums.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,14 +18,9 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import TYPE_CHECKING +# SOFTWARE. -if TYPE_CHECKING: - from enum import Enum -else: - from fastenum.fastenum import Enum +from enum import Enum class VerificationLevel(Enum): @@ -239,6 +235,7 @@ class ChannelType(Enum): GUILD_STAGE_VOICE = 13 GUILD_DIRECTORY = 14 GUILD_FORUM = 15 + GUILD_MEDIA = 16 class VideoQualityMode(Enum): @@ -251,6 +248,12 @@ class SortOrderType(Enum): CREATION_DATE = 1 +class ForumLayoutType(Enum): + NOT_SET = 0 + LIST_VIEW = 1 + GALLERY_VIEW = 2 + + class MessageType(Enum): DEFAULT = 0 RECIPIENT_ADD = 1 @@ -286,12 +289,12 @@ class MessageActivityType(Enum): class EmbedType(Enum): - rich = 'rich' - image = 'image' - video = 'video' - gif = 'gifv' - article = 'article' - link = 'link' + rich = "rich" + image = "image" + video = "video" + gif = "gifv" + article = "article" + link = "link" class GuildScheduledEventPrivacyLevel(Enum): diff --git a/pycord/errors.py b/pycord/errors.py index 2821a516..39b4cb96 100644 --- a/pycord/errors.py +++ b/pycord/errors.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,89 +18,159 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. from typing import Any from aiohttp import ClientResponse -from .utils import parse_errors - class PycordException(Exception): - pass - + """ + The base exception class for Pycord. -class BotException(PycordException): + All exceptions that Pycord raises are subclasses of this. + """ pass -class InteractionException(BotException): - pass - - -class GatewayException(PycordException): - pass - - -class NoIdentifiesLeft(GatewayException): +class BotException(PycordException): + """ + The base exception class for bot-related errors. + """ pass -class DisallowedIntents(GatewayException): - pass +# HTTP +def _flatten_errors(errors: dict[str, Any], key: str = "") -> dict[str, list[str]]: + items = {} + for subkey, value in errors.items(): + if subkey == "_errors": + items[key] = [error["message"] for error in value] + continue + elif isinstance(value, dict): + items.update(_flatten_errors(value, f"{key}.{subkey}" if key else subkey)) + else: + items[f"{key}.{subkey}"] = value + return items -class ShardingRequired(GatewayException): - pass +class HTTPException(PycordException): + """ + A generic exception for when an HTTP request fails. + + Attributes + ---------- + response: :class:`aiohttp.ClientResponse` + The response of the failed HTTP request. + status: :class:`int` + The status code of the HTTP request. + response_data: :class:`str` | :class:`dict[str, Any]` | None + The data returned from the HTTP request. + request_data: :class:`dict[str, Any]` | :class:`list[Any]` | :class:`bytes` | None + The data that was sent in the HTTP request. + errors: dict[:class:`str`, list[:class:`str`]] | None + The errors that were returned from the HTTP request. + Keys are the location of the error in request data, + using dot notation, and values are lists of error messages. + message: :class:`str` | None + The main error text indicating what went wrong. + code: :class:`int` | None + The Discord-specific error code that was returned as a result + of the request. See the `Discord API documentation + `_ + for more info. + """ + def __init__( + self, + response: ClientResponse, + response_data: str | dict[str, Any] | bytes | None, + request_data: dict[str, Any] | list[Any] | None, + ) -> None: + self.response: ClientResponse = response + self.status: int = response.status + self.response_data: str | dict[str, Any] | bytes | None = response_data + self.request_data: dict[str, Any] | list[Any] | None = request_data + self.errors: dict[str, list[str]] | None = _flatten_errors(response_data) if isinstance(response_data, dict) else None + if isinstance(response_data, dict) and "message" in response_data: + self.message: str | None = response_data["message"] + self.code: int | None = response_data.get("code") + else: + self.message: str | None = response_data + self.code: int | None = None + help_text: str = self.message or "" + for error, messages in self.errors.items(): + help_text += f"\nIn {error}: {', '.join(messages)}" + errcode: str = f" (error code: {self.code})" if self.code else "" + super().__init__(f"{self.status} {response.reason}{errcode} {help_text}") + + @property + def problem_values(self) -> dict[str, Any]: + problems = {} + for error in self.errors.keys(): + path = error.split(".") + current = self.request_data + for key in path: + if isinstance(current, list): + current = current[int(key)] + elif isinstance(current, dict): + current = current[key] + problems[error] = current + return problems -class InvalidAuth(GatewayException): - pass +class Forbidden(HTTPException): + """ + Exception that is raised for when status code 403 occurs. + This is a subclass of :exc:`HTTPException`. + """ -class HTTPException(PycordException): - def __init__(self, resp: ClientResponse, data: dict[str, Any] | None) -> None: - self._response = resp - self.status = resp.status - if data: - self.code = data.get('code', 0) - self.error_message = data.get('message', '') +class NotFound(HTTPException): + """ + Exception that is raised for when status code 404 occurs. - if errors := data.get('errors'): - self.errors = parse_errors(errors) - message = self.error_message + '\n'.join( - f'In {key}: {err}' for key, err in self.errors.items() - ) - else: - message = self.error_message + This is a subclass of :exc:`HTTPException`. + """ - super().__init__(f'{resp.status} {resp.reason} (code: {self.code}): {message}') +class DiscordException(HTTPException): + """ + Exception that is raised for when status code in the 5xx range occurs. -class Forbidden(HTTPException): - pass + This is a subclass of :exc:`HTTPException`. + """ -class NotFound(HTTPException): - pass +# Gateway +class ShardingRequired(BotException): + """ + Discord is requiring your bot to use sharding. + For more information, see the + `Discord API documentation `_. -class InternalError(HTTPException): + This is a subclass of :exc:`BotException`. + """ pass -class OverfilledShardsException(BotException): - pass - +class InvalidAuth(BotException): + """ + Your bot token is invalid. -class FlagException(BotException): + This is a subclass of :exc:`BotException`. + """ pass -class ComponentException(BotException): - pass +class DisallowedIntents(BotException): + """ + You are using privledged intents that you have not been approved for. + For more information, see the + `Discord API documentation `_. -class NoFetchOrGet(BotException): + This is a subclass of :exc:`BotException`. + """ pass diff --git a/pycord/events/__init__.py b/pycord/events/__init__.py deleted file mode 100644 index ffe3ab5e..00000000 --- a/pycord/events/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -pycord.events -~~~~~~~~~~~~~ -Basic Implementation of Discord Events - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .channels import * -from .event_manager import * -from .guilds import * -from .other import * diff --git a/pycord/events/channels.py b/pycord/events/channels.py deleted file mode 100644 index 48d1ec8d..00000000 --- a/pycord/events/channels.py +++ /dev/null @@ -1,144 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import TYPE_CHECKING, Any - -from ..channel import CHANNEL_TYPE, identify_channel -from ..message import Message -from ..snowflake import Snowflake -from .event_manager import Event -from .guilds import _GuildAttr - -if TYPE_CHECKING: - from ..state import State - - -class ChannelCreate(Event): - _name = 'CHANNEL_CREATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - channel = identify_channel(data, state) - - deps = [channel.guild_id] if hasattr(channel, 'guild_id') else [] - await (state.store.sift('channels')).save(deps, channel.id, channel) - self.channel = channel - - -class ChannelUpdate(Event): - _name = 'CHANNEL_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - channel = identify_channel(data, state) - - deps = [channel.guild_id] if hasattr(channel, 'guild_id') else [] - self.previous: CHANNEL_TYPE = await (state.store.sift('channels')).save( - deps, channel.id, channel - ) - self.channel = channel - - -class ChannelDelete(Event): - _name = 'CHANNEL_DELETE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - channel = identify_channel(data, state) - - deps = [channel.guild_id] if hasattr(channel, 'guild_id') else [] - res = await (state.store.sift('channels')).discard(deps, channel.id) - - self.cached = res - self.channel = channel - - -class ChannelPinsUpdate(_GuildAttr): - _name = 'CHANNEL_PINS_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.channel_id: Snowflake = Snowflake(data.get('channel_id')) - self.guild_id: Snowflake = Snowflake(data.get('guild_id')) - - -PinsUpdate = ChannelPinsUpdate - - -class MessageCreate(Event): - _name = 'MESSAGE_CREATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - message = Message(data, state) - self.message = message - self.is_human = message.author.bot is False - self.content = self.message.content - - await (state.store.sift('messages')).save( - [message.channel_id], message.id, message - ) - - -class MessageUpdate(Event): - _name = 'MESSAGE_UPDATE' - - previous: Message | None - message: Message - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.previous: Message | None = await (state.store.sift('messages')).get_one( - [int(data['channel_id'])], int(data['id']) - ) - if self.previous is None: - message = Message(data, state) - await state.store.sift('messages').insert( - [message.channel_id], message.id, message - ) - else: - message: Message = self.previous - message._modify_from_cache(**data) - - self.message = message - - -class MessageDelete(Event): - _name = 'MESSAGE_DELETE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.message_id: Snowflake = Snowflake(data['id']) - self.channel_id: Snowflake = Snowflake(data['channel_id']) - self.guild_id: Snowflake | None = ( - Snowflake(data['guild_id']) if 'guild_id' in data else None - ) - - self.message: Message | None = await (state.store.sift('messages')).discard( - [self.channel_id], self.message_id - ) - - -class MessageBulkDelete(Event): - _name = 'MESSAGE_DELETE_BULK' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - channel_id = Snowflake(data['channel_id']) - bulk: list[Message | int] = [ - (await (state.store.sift('messages')).discard([channel_id], message_id)) - or message_id - for message_id in (Snowflake(id) for id in data['ids']) - ] - self.deleted_messages = bulk - self.length = len(bulk) diff --git a/pycord/events/event_manager.py b/pycord/events/event_manager.py deleted file mode 100644 index 7eba4eea..00000000 --- a/pycord/events/event_manager.py +++ /dev/null @@ -1,103 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -import asyncio -from asyncio import Future -from typing import TYPE_CHECKING, Any, Type, TypeVar - -from ..types import AsyncFunc - -if TYPE_CHECKING: - from ..state import State - - -T = TypeVar('T', bound='Event') - - -class Event: - _name: str - _state: 'State' - - async def _is_publishable(self, data: dict[str, Any], state: 'State') -> bool: - return True - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - ... - - -class EventManager: - def __init__(self, base_events: list[Type[Event]], state: 'State') -> None: - self._base_events = base_events - self._state = state - - # structured like: - # EventClass: [childrenfuncs] - self.events: dict[Type[Event], list[AsyncFunc]] = {} - - for event in self._base_events: - # base_events is used for caching purposes - self.events[event] = [] - - self.wait_fors: dict[Type[Event], list[Future]] = {} - - def add_event(self, event: Type[Event], func: AsyncFunc) -> None: - try: - self.events[event].append(func) - except KeyError: - self.events[event] = [func] - - def wait_for(self, event: Type[T]) -> Future[T]: - fut = Future() - - try: - self.wait_fors[event].append(fut) - except KeyError: - self.wait_fors[event] = [fut] - - return fut - - async def publish(self, event_str: str, data: dict[str, Any]) -> None: - # in certain cases, events may be inserted during runtime which breaks dispatching - items = list(self.events.items()) - - for event, funcs in items: - if event._name == event_str: - eve = event() - dispatch = await eve._is_publishable(data, self._state) - - # used in cases like GUILD_AVAILABLE - if dispatch is False: - continue - else: - await eve._async_load(data, self._state) - - eve._state = self._state - - for func in funcs: - asyncio.create_task(func(eve)) - - wait_fors = self.wait_fors.get(event) - - if wait_fors is not None: - for wait_for in wait_fors: - wait_for.set_result(eve) - self.wait_fors.pop(event) diff --git a/pycord/events/guilds.py b/pycord/events/guilds.py deleted file mode 100644 index e6cb3370..00000000 --- a/pycord/events/guilds.py +++ /dev/null @@ -1,314 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -import functools -from typing import TYPE_CHECKING, Any - -from ..channel import Channel, Thread, identify_channel -from ..guild import Guild -from ..member import Member -from ..role import Role -from ..scheduled_event import ScheduledEvent -from ..snowflake import Snowflake -from ..stage_instance import StageInstance -from ..user import User -from .event_manager import Event - -if TYPE_CHECKING: - from ..state import State - - -class _GuildAttr(Event): - guild_id: int | None - - @functools.cached_property - async def guild(self) -> Guild | None: - if self.guild_id is None: - return None - - guild = await (self._state.store.sift('guilds')).get_one( - [self.guild_id], self.guild_id - ) - - return guild - - -class _MemberAttr(Event): - user_id: int | None - guild_id: int | None - - @functools.cached_property - async def member(self) -> Guild | None: - if self.user_id is None: - return None - - member = await (self._state.store.sift('members')).get_one( - [self.guild_id], self.user_id - ) - - return member - - -class GuildCreate(Event): - _name = 'GUILD_CREATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> bool: - self.guild = Guild(data, state=state) - self.channels: list[Channel] = [ - identify_channel(c, state) for c in data['channels'] - ] - self.threads: list[Thread] = [ - identify_channel(c, state) for c in data['threads'] - ] - self.stage_instances: list[StageInstance] = [ - StageInstance(st, state) for st in data['stage_instances'] - ] - self.guild_scheduled_events: list[ScheduledEvent] = [ - ScheduledEvent(se, state) for se in data['guild_scheduled_events'] - ] - - await (state.store.sift('guilds')).save( - [self.guild.id], self.guild.id, self.guild - ) - - for channel in self.channels: - await (state.store.sift('channels')).save( - [self.guild.id], channel.id, channel - ) - - for thread in self.threads: - await (state.store.sift('threads')).save( - [self.guild.id, thread.parent_id], thread.id, thread - ) - - for stage in self.stage_instances: - await (state.store.sift('stages')).save( - [stage.channel_id, self.guild.id, stage.guild_scheduled_event_id], - stage.id, - stage, - ) - - for scheduled_event in self.guild_scheduled_events: - await (state.store.sift('scheduled_events')).save( - [ - scheduled_event.channel_id, - scheduled_event.creator_id, - scheduled_event.entity_id, - self.guild.id, - ], - scheduled_event.id, - scheduled_event, - ) - - -class GuildAvailable(GuildCreate): - """ - Event denoting the accessibility of a previously joined Guild. - """ - - async def _is_publishable(self, data: dict[str, Any], state: 'State') -> bool: - return True if int(data['id']) in state._available_guilds else False - - -class GuildJoin(GuildCreate): - async def _is_publishable(self, data: dict[str, Any], state: 'State') -> bool: - exists = True if int(data['id']) in state._available_guilds else False - - if exists: - return False - - state._available_guilds.append(int(data['id'])) - return True - - -class GuildUpdate(Event): - _name = 'GUILD_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild = Guild(data=data, state=state) - res = await (state.store.sift('guilds')).save([guild.id], guild.id, guild) - - self.previous = res - - self.guild = guild - - -class GuildDelete(Event): - _name = 'GUILD_DELETE' - - async def _is_publishable(self, data: dict[str, Any], _state: 'State') -> bool: - if data.get('unavailable', None) is not None: - return True - else: - return False - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id = Snowflake(data['guild_id']) - res = await (state.store.sift('guilds')).discard([guild_id], guild_id, Guild) - - self.guild = res - self.guild_id = guild_id - - -class GuildBanCreate(_GuildAttr): - _name = 'GUILD_BAN_ADD' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id: Snowflake = Snowflake(data['guild_id']) - - self.guild_id = guild_id - self.user = User(data['user']) - - -GuildBanAdd = GuildBanCreate -BanAdd = GuildBanCreate - - -class GuildBanDelete(_GuildAttr): - _name = 'GUILD_BAN_REMOVE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id: Snowflake = Snowflake(data['guild_id']) - - self.guild_id = guild_id - self.user = User(data['user']) - - -BanDelete = GuildBanDelete - - -class GuildMemberAdd(_GuildAttr): - _name = 'GUILD_MEMBER_ADD' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id = Snowflake(data['guild_id']) - member = Member(data, state, guild_id=guild_id) - if state.cache_guild_members: - await (state.store.sift('members')).insert( - [guild_id], member.user.id, member - ) - - self.guild_id = guild_id - self.member: Member = member - - -MemberJoin = GuildMemberAdd - - -class GuildMemberUpdate(_GuildAttr): - _name = 'GUILD_MEMBER_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id = Snowflake(data['guild_id']) - member = Member(data, state, guild_id=guild_id) - - res = await (state.store.sift('members')).save( - [guild_id], member.user.id, member - ) - - self.member: Member = res - self.guild_id = guild_id - self.previous = res - - -MemberEdit = GuildMemberUpdate - - -class GuildMemberRemove(_GuildAttr): - _name = 'GUILD_MEMBER_REMOVE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self.user_id: Snowflake = Snowflake(data['user']['id']) - - self.user = User(data['user'], state) - - await (state.store.sift('members')).discard([self.guild_id], self.user_id) - - -MemberRemove = GuildMemberRemove - - -class GuildMemberChunk(Event): - _name = 'GUILD_MEMBER_CHUNK' - - async def _is_publishable(self, _data: dict[str, Any], _state: 'State') -> bool: - return False - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id: Snowflake = Snowflake(data['guild_id']) - ms: list[Member] = [ - await (state.store.sift('members')).save([guild_id], member.user.id, member) - for member in ( - Member(member_data, state, guild_id=guild_id) - for member_data in data['members'] - ) - ] - self.members = ms - - -MemberChunk = GuildMemberChunk - - -class GuildRoleCreate(_GuildAttr): - _name = 'GUILD_ROLE_CREATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.guild_id: Snowflake = Snowflake(data['guild_id']) - role = Role(data['role'], state) - - await (state.store.sift('roles')).insert([self.guild_id], role.id, role) - - self.role = role - - -RoleCreate = GuildRoleCreate - - -class GuildRoleUpdate(_GuildAttr): - _name = 'GUILD_ROLE_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - guild_id: Snowflake = Snowflake(data['guild_id']) - role = Role(data['role'], self) - - await (state.store.sift('roles')).save([guild_id], role.id, role) - - self.role = role - - -RoleUpdate = GuildRoleUpdate - - -class GuildRoleDelete(_GuildAttr): - _name = 'GUILD_ROLE_DELETE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self.role_id: Snowflake = Snowflake(data['role_id']) - - self.role: Role | None = await (state.store.sift('roles')).discard( - [self.guild_id], self.role_id - ) - - -RoleDelete = GuildRoleDelete diff --git a/pycord/events/other.py b/pycord/events/other.py deleted file mode 100644 index cac1829a..00000000 --- a/pycord/events/other.py +++ /dev/null @@ -1,117 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -import asyncio -from typing import TYPE_CHECKING, Any, Type - -import typing_extensions - -from ..interaction import Interaction -from ..user import User -from .event_manager import Event - -if TYPE_CHECKING: - from ..state import State - - -class Ready(Event): - _name = 'READY' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> bool: - state._available_guilds: list[int] = [int(uag['id']) for uag in data['guilds']] - - user = User(data['user'], state) - state.user = user - self.user = user - - if not state._ready: - if hasattr(state, '_raw_user_fut'): - state._raw_user_fut.set_result(None) - - state._ready = True - - for gear in state.gears: - asyncio.create_task( - gear.on_attach(), name=f'Attaching Gear: {gear.name}' - ) - - state.application_commands = [] - state.application_commands.extend( - await state.http.get_global_application_commands(state.user.id, True) - ) - state._application_command_names: list[str] = [] - - for command in state.commands: - await command.instantiate() - state.event_manager.add_event(command._processor_event, command._invoke) - if hasattr(command, 'name'): - state._application_command_names.append(command.name) - - for app_command in state.application_commands: - if app_command['name'] not in state._application_command_names: - await state.http.delete_global_application_command( - state.user.id.real, app_command['id'] - ) - - -class Hook(Event): - _name = 'READY' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> bool: - if state._ready is True: - return False - - user = User(data['user'], state) - self.user = user - - -class UserUpdate(Event): - _name = 'USER_UPDATE' - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - self.user = User(data, state) - state.user = self.user - state.raw_user = data - - -class InteractionCreate(Event): - _name = 'INTERACTION_CREATE' - - def __init__(self, interaction: Type[Interaction] = Interaction) -> None: - self._interaction_object = interaction - - def __call__(self) -> typing_extensions.Self: - """ - Function to circumnavigate the event manager's event() - """ - return self - - async def _async_load(self, data: dict[str, Any], state: 'State') -> None: - interaction = self._interaction_object(data, state, True) - - for component in state.components: - asyncio.create_task(component._invoke(interaction)) - - for modal in state.modals: - asyncio.create_task(modal._invoke(interaction)) - - self.interaction = interaction diff --git a/pycord/ext/__init__.py b/pycord/ext/__init__.py index 87d7fef7..21e8c486 100644 --- a/pycord/ext/__init__.py +++ b/pycord/ext/__init__.py @@ -1,8 +1,8 @@ """ pycord.ext ~~~~~~~~~~ -A variety of Pycord extensions to make your bot development experience easier +Officially vendored extensions part of Pycord. -:copyright: 2021-present Pycord Development +:copyright: 2021-present Pycord :license: MIT """ diff --git a/pycord/ext/gears/__init__.py b/pycord/ext/gears/__init__.py deleted file mode 100644 index d0acae90..00000000 --- a/pycord/ext/gears/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -pycord.ext.gears -~~~~~~~~~~~~~~~~ -Extension to add a way to extend your bot easily throughout multiple files. - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .gear import * diff --git a/pycord/ext/gears/gear.py b/pycord/ext/gears/gear.py deleted file mode 100644 index c375290a..00000000 --- a/pycord/ext/gears/gear.py +++ /dev/null @@ -1,173 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar - -from ...commands import Command, Group -from ...commands.application.command import ApplicationCommand -from ...events.event_manager import Event -from ...missing import MISSING, MissingEnum -from ...types import AsyncFunc - -if TYPE_CHECKING: - from ...bot import Bot - - -T = TypeVar('T') -AF = TypeVar('AF', AsyncFunc) - - -class BaseContext(SimpleNamespace): - ... - - -ContextT = TypeVar('ContextT', bound=BaseContext) - - -class Gear(Generic[ContextT]): - """ - The Gear. A set of commands & events separate from Bot. - Useful for multi-file separation for huge bots. - - Parameters - ---------- - name: :class:`str` - The name of this Gear. - - Attributes - ---------- - bot: Union[:class:`pycord.Bot`, None] - The bot this Gear is attached to. - """ - - ctx: ContextT - - __slots__ = ('_listener_functions', '_commands', 'name', 'ctx', 'bot') - - def __init__(self, name: str, ctx: ContextT | None = None) -> None: - self.name = name - self._listener_functions: dict[Type[Event], list[AsyncFunc]] = {} - self.bot: Bot - self._commands: list[Command | Group] = [] - if ctx is None: - self.ctx = BaseContext() - else: - self.ctx = ctx - - async def on_attach(self, *args, **kwargs) -> None: - ... - - def listen(self, name: str) -> T: - """ - Listen to an event - - Parameters - ---------- - name: :class:`str` - The name of the event to listen to. - """ - - def wrapper(func: T) -> T: - if self._listener_functions.get(name): - self._listener_functions[name].append(func) - else: - self._listener_functions[name] = [func] - return func - - return wrapper - - def command( - self, - name: str | MissingEnum = MISSING, - cls: Type[T] = ApplicationCommand, - **kwargs: Any, - ) -> T: - """ - Create a command within the Gear - - Parameters - ---------- - name: :class:`str` - The name of the Command. - cls: type of :class:`.commands.Command` - The command type to instantiate. - kwargs: dict[str, Any] - The kwargs to entail onto the instantiated command. - """ - - def wrapper(func: AsyncFunc) -> Command: - command = cls(func, state=None, name=name, **kwargs) - self._commands.append(command) - return command - - return wrapper - - def group(self, name: str, cls: Type[Group], **kwargs: Any) -> Callable[[AF], AF]: - """ - Create a brand-new Group of Commands - - Parameters - ---------- - name: :class:`str` - The name of the Group. - cls: type of :class:`.commands.Group` - The group type to instantiate. - kwargs: dict[str, Any] - The kwargs to entail onto the instantiated group. - """ - - def wrapper(func: AF) -> AF: - # I know this partially ruins typing, but - # Gears are loaded before events are taken in, so - # theoretically nothing can break with state being None. - r = cls(func, name, None, **kwargs) # type: ignore - self._commands.append(r) - return r - - return wrapper - - def attach(self, bot: Bot) -> None: - """ - Attaches this Gear to a bot. - - Parameters - ---------- - - bot: :class:`pycord.Bot` - The bot to attach onto - """ - - for name, funcs in self._listener_functions.items(): - for func in funcs: - bot._state.event_manager.add_event(name, func) - - for cmd in self._commands: - if isinstance(cmd, Command): - cmd._state = bot._state - cmd._state.commands.append(cmd) - elif isinstance(cmd, Group): - cmd._state = bot._state - - self.bot = bot - - self.bot._state.gears.append(self) diff --git a/pycord/ext/tasks/__init__.py b/pycord/ext/tasks/__init__.py new file mode 100644 index 00000000..c57f4cb0 --- /dev/null +++ b/pycord/ext/tasks/__init__.py @@ -0,0 +1,230 @@ +""" +pycord.ext.tasks +~~~~~~~~~~~~~~~~ +Pycord utility extension for an extended asyncio task system. + +:copyright: 2021-present Pycord +:license: MIT +""" + +import asyncio +import inspect +import logging +import time +import traceback +from datetime import timedelta +from typing import Callable, Generic, TypeVar + +from ...custom_types import AsyncFunc + +T = TypeVar("T") + +_log = logging.getLogger(__name__) + + +class PycordTask(Generic[T]): + """Class for handling a repeatable task + + Attributes + ---------- + interval: :class:`datetime.timedelta` + The interval in which this task repeats in. + interval_secs: :class:`float` + The number of seconds this tasks repeats. + loop_started_at: :class:`float` + The `time.time` result of when the loop started. + running: :class:`bool` + Whether the loop is currently running. + failed: :class:`bool` + Whether the loop has failed. + count: :class:`int` + The amount of times the loop should repeat. + """ + + def __init__( + self, callback: T, interval: timedelta, count: int | None = None + ) -> None: + self._callback = callback + self.interval = interval + self.interval_secs = interval.total_seconds() + self.loop_started_at: float | None = None + self.running: bool = False + self.failed: bool = False + self._running_loop_task = None + self._on_error_coro = None + self._pre_loop_coro = None + self._post_loop_coro = None + self.count = count + + self.__doc__ = self._callback.__doc__ + + def error(self, coro: AsyncFunc) -> None: + """A decorator for handling exceptions raised by the callback""" + + if not inspect.iscoroutinefunction(coro): + raise TypeError( + "PycordTask.error handling only accepts coroutine functions" + ) + + self._on_error_coro = coro + + def pre_loop(self, coro: AsyncFunc) -> None: + """A decorator for running a function before a task. + Could be useful to setup some sort of state. + """ + + if not inspect.iscoroutinefunction(coro): + raise TypeError("PycordTask.pre_loop only accepts coroutine functions") + + self._pre_loop_coro = coro + + def post_loop(self, coro: AsyncFunc) -> None: + """A decorator for running a function after a task. + Could be useful for state cleanup. + """ + + if not inspect.iscoroutinefunction(coro): + raise TypeError("PycordTask.post_loop only accepts coroutine functions") + + self._post_loop_coro = coro + + def change_interval( + self, + *, + microseconds: int = 0, + milliseconds: int = 0, + seconds: int = 0, + minutes: int = 0, + hours: int = 0, + days: int = 0, + weeks: int = 0, + ) -> None: + """Change the interval in which this task runs in + + .. warning:: + Do not run this too often as it could make tasks run more inconsistent. + """ + + self.interval = timedelta( + microseconds=microseconds, + milliseconds=milliseconds, + seconds=seconds, + minutes=minutes, + hours=hours, + days=days, + weeks=weeks, + ) + self.interval_secs = self.interval.total_seconds() + + def start(self) -> None: + """Start the loop. Should be done *after* the asyncio loop or your bot is started.""" + + _log.debug(f"[{self._callback}] starting loop") + self._running_loop_task = asyncio.create_task(self._loop()) + self.loop_started_at = int(time.time()) + + async def _loop(self) -> None: + self.running = True + await asyncio.sleep(self.interval_secs) + _log.debug(f"[{self._callback}] starting new loop") + if self._pre_loop_coro: + _log.debug(f"[{self._callback}] doing pre-loop setup") + await self._pre_loop_coro() + + try: + _log.debug(f"[{self._callback}] calling function") + await self._callback() + except Exception as exc: + _log.debug( + f"[{self._callback}] function finished with error:\n\n{traceback.format_exc()}" + ) + if self._on_error_coro: + _log.debug(f"[{self._callback}] error handled") + await self._on_error_coro(exc) + else: + _log.debug(f"[{self._callback}] loop failed") + self.running = False + self._running_loop_task = None + self.failed = True + raise exc + else: + _log.debug(f"[{self._callback}] function finished without any errors") + + if self._post_loop_coro: + _log.debug(f"[{self._callback}] doing post-loop cleanup") + await self._post_loop_coro() + + _log.debug(f"[{self._callback}] re-running loop") + + if self.count: + self.count -= 1 + if self.count == 0: + return + + self._running_loop_task = asyncio.create_task(self._loop()) + + def cancel(self) -> bool: + """Cancel the current loop + + Returns + ------- + True: + The loop is running and was canceled successfully. + False: + The loop isn't running and wasn't canceled. + """ + + _log.debug(f"[{self._callback}] canceling loop") + if not self.running and self._running_loop_task is None: + return False + + self._running_loop_task.cancel() + self._running_loop_task = None + self.running = False + return True + + def restart(self) -> None: + """Restart the current loop""" + + _log.debug(f"[{self._callback}] prematurely restarting loop") + self.cancel() + self.start() + + def __call__(self, *args, **kwargs): + return self._callback(*args, **kwargs) + + +def loop( + *, + microseconds: int = 0, + milliseconds: int = 0, + seconds: int = 0, + minutes: int = 0, + hours: int = 0, + days: int = 0, + weeks: int = 0, + count: int | None = None, +) -> Callable[[T], PycordTask[T]]: + """A decorator to create a task from a callback. + + Takes the same parameters as timedelta with an added `count` field + to determine how much times the task should repeat. + """ + + if count is not None and count <= 0: + raise ValueError("Count cannot be negative or 0") + + interval = timedelta( + microseconds=microseconds, + milliseconds=milliseconds, + seconds=seconds, + minutes=minutes, + hours=hours, + days=days, + weeks=weeks, + ) + + def decorator(coro: T) -> PycordTask[T]: + return PycordTask[coro](coro, interval, count) + + return decorator diff --git a/pycord/file.py b/pycord/file.py index ceef3099..b4bdc1fc 100644 --- a/pycord/file.py +++ b/pycord/file.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,7 +18,7 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. import asyncio @@ -27,12 +28,12 @@ def _open_file(path: pathlib.Path) -> BinaryIO: - return path.expanduser().open('rb') + return path.expanduser().open("rb") class File(Protocol): path: pathlib.Path | None - filename: str + filename: str | None file: BinaryIO spoiler: bool @@ -56,17 +57,16 @@ def __init__( async def _hook_file(self) -> None: loop = asyncio.get_running_loop() - self.file = await loop.run_in_executor(None, _open_file, _open_file, self._path) - - # assure we have control over closures - self._closer = self.file.close - self.file.close = lambda: None + self.file = await loop.run_in_executor(None, _open_file, _open_file, self.path) if self.filename is None: self.filename = self.path.name - if self.spoiler and not self.filename.startswith('SPOILER_'): - self.filename = f'SPOILER_{self.filename}' + if self.spoiler and not self.filename.startswith("SPOILER_"): + self.filename = f"SPOILER_{self.filename}" + + self._closer = self.file.close + self.file.close = lambda: None # type: ignore[method-assign] self._original_position = self.file.tell() @@ -74,6 +74,7 @@ def reset(self, seek: int | bool = True) -> None: if seek: self.file.seek(self._original_position) + # TODO: circumvent mypy, and safely reassign the .close method def close(self) -> None: self.file.close = self._closer self._closer() @@ -82,14 +83,16 @@ def close(self) -> None: class BytesFile(File): def __init__(self, filename: str, io: bytes | BytesIO) -> None: self.filename = filename - self.file = BytesIO(io) + if not isinstance(io, BytesIO): + self.file = BytesIO(io) + else: + self.file = io - # assure we have control over closures - self._closer = self.file.close - self.file.close = lambda: None + if self.spoiler and not self.filename.startswith("SPOILER_"): + self.filename = f"SPOILER_{self.filename}" - if self.spoiler and not self.filename.startswith('SPOILER_'): - self.filename = f'SPOILER_{self.filename}' + self._closer = self.file.close + self.file.close = lambda: None # type: ignore[method-assign] self._original_position = self.file.tell() diff --git a/pycord/flags.py b/pycord/flags.py index 74da143d..e94a1b62 100644 --- a/pycord/flags.py +++ b/pycord/flags.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,31 +18,34 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. from __future__ import annotations from collections.abc import Callable -from typing import Sequence, Type, TypeVar +from typing import Self, Sequence, Type, TypeVar -from .errors import FlagException +from mypy_extensions import mypyc_attr, trait -F = TypeVar('F', bound='Flags') -FF = TypeVar('FF') +F = TypeVar("F", bound="Flags") __all__: Sequence[str] = ( - 'Intents', - 'Permissions', - 'ChannelFlags', - 'MessageFlags', - 'SystemChannelFlags', - 'ApplicationFlags', - 'UserFlags', + "Intents", + "Permissions", + "ChannelFlags", + "MessageFlags", + "SystemChannelFlags", + "ApplicationFlags", + "ChannelFlags", + "MessageFlags", + "UserFlags", + "MemberFlags", + "RoleFlags", ) class flag: - def __init__(self, func: Callable): + def __init__(self, func: Callable[..., int]): self.value: int = func(None) self.__doc__ = func.__doc__ self._name = func.__name__ @@ -73,8 +77,10 @@ def wrapper(cls: Type[F]) -> Type[F]: return wrapper +@trait +@mypyc_attr(allow_interpreted_subclasses=True) class Flags: - _FLAGS = {} + _FLAGS: dict[str, int] = {} def __init__(self, **flags_named: bool) -> None: self._values: dict[str, bool] = {} @@ -83,8 +89,8 @@ def __init__(self, **flags_named: bool) -> None: try: self._FLAGS[name] except KeyError: - raise FlagException( - f'Flag {name} is not a valid flag of {self.__class__}' + raise RuntimeError( + f"Flag {name} is not a valid flag of {self.__class__}" ) if set is False: @@ -93,7 +99,7 @@ def __init__(self, **flags_named: bool) -> None: self._values[name] = set @classmethod - def from_value(cls: Type[FF], value: int | str) -> FF: + def from_value(cls, value: int | str) -> Self: self = cls() value = int(value) @@ -398,6 +404,10 @@ def suppress_join_notifications_replies(self) -> bool | int: @fill() class ApplicationFlags(Flags): + @flag + def application_auto_moderation_rule_create_badge(self) -> bool | int: + return 1 << 6 + @flag def gateway_presence(self) -> bool | int: return 1 << 12 @@ -569,3 +579,17 @@ def bypasses_verification(self) -> bool | int: @flag def started_onboarding(self) -> bool | int: return 1 << 3 + + +@fill() +class RoleFlags(Flags): + @flag + def in_prompt(self) -> bool | int: + return 1 << 0 + + +@fill() +class AttachmentFlags(Flags): + @flag + def is_remix(self) -> bool | int: + return 1 << 2 diff --git a/pycord/gateway/__init__.py b/pycord/gateway/__init__.py deleted file mode 100644 index 76b48910..00000000 --- a/pycord/gateway/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -pycord.gateway -~~~~~~~~~~~~~~ -Advanced Implementation of the Discord Gateway - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from ..events.event_manager import * -from .cluster import * -from .manager import * -from .notifier import * -from .passthrough import * -from .shard import * diff --git a/pycord/gateway/cluster.py b/pycord/gateway/cluster.py deleted file mode 100644 index 31f0374a..00000000 --- a/pycord/gateway/cluster.py +++ /dev/null @@ -1,71 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -import asyncio -from multiprocessing import Process -from typing import TYPE_CHECKING - -from aiohttp import BasicAuth - -from ..utils import chunk -from .manager import ShardManager - -if TYPE_CHECKING: - from ..state import State - - -class ShardCluster(Process): - def __init__( - self, - state: State, - shards: list[int], - amount: int, - managers: int, - proxy: str | None = None, - proxy_auth: BasicAuth | None = None, - ) -> None: - self.shard_managers: list[ShardManager] = [] - self._state = state - self._shards = shards - self._amount = amount - self._managers = managers - self._proxy = proxy - self._proxy_auth = proxy_auth - super().__init__() - - async def _run(self) -> None: - await self._state._cluster_lock.acquire() - # this is guessing that `i` is a shard manager - tasks = [] - for sharder in list(chunk(self._shards, self._managers)): - manager = ShardManager( - self._state, sharder, self._amount, self._proxy, self._proxy_auth - ) - tasks.append(manager.start()) - self.shard_managers.append(manager) - asyncio.create_task(manager.start()) - self._state._cluster_lock.release() - self.keep_alive = asyncio.Future() - await self.keep_alive - - def run(self) -> None: - asyncio.create_task(self._run()) diff --git a/pycord/gateway/manager.py b/pycord/gateway/manager.py deleted file mode 100644 index d678b5f5..00000000 --- a/pycord/gateway/manager.py +++ /dev/null @@ -1,104 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -from aiohttp import BasicAuth, ClientSession - -from ..errors import NoIdentifiesLeft - -if TYPE_CHECKING: - from ..state import State - -from .notifier import Notifier -from .passthrough import PassThrough -from .shard import Shard - - -class ShardManager: - def __init__( - self, - state: State, - shards: list[int], - amount: int, - proxy: str | None = None, - proxy_auth: BasicAuth | None = None, - ) -> None: - self.shards: list[Shard] = [] - self.amount = amount - self._shards = shards - self._state = state - self.proxy = proxy - self.proxy_auth = proxy_auth - - def add_shard(self, shard: Shard) -> None: - self.shards.insert(shard.id, shard) - - def remove_shard(self, shard: Shard) -> None: - self.shards.remove(shard) - - def remove_shards(self) -> None: - self.shards.clear() - - async def delete_shard(self, shard: Shard) -> None: - shard._receive_task.cancel() - shard._hb_task.cancel() - - await shard._ws.close() - self.remove_shard(shard) - - async def delete_shards(self) -> None: - for shard in self.shards: - await self.delete_shard(shard=shard) - - async def start(self) -> None: - self.session = ClientSession() - notifier = Notifier(self) - - if not self._state.shard_concurrency: - info = await self._state.http.get_gateway_bot() - session_start_limit = info['session_start_limit'] - - if session_start_limit['remaining'] == 0: - raise NoIdentifiesLeft('session_start_limit has been exhausted') - - self._state.shard_concurrency = PassThrough( - session_start_limit['max_concurrency'], 7 - ) - self._state._session_start_limit = session_start_limit - - tasks = [] - - for shard_id in self._shards: - shard = Shard( - id=shard_id, state=self._state, session=self.session, notifier=notifier - ) - - tasks.append(shard.connect(token=self._state.token)) - - self.shards.append(shard) - - await asyncio.gather(*tasks) - - async def shutdown(self) -> None: - await self.delete_shards() diff --git a/pycord/gateway/shard.py b/pycord/gateway/shard.py deleted file mode 100644 index d02e2ada..00000000 --- a/pycord/gateway/shard.py +++ /dev/null @@ -1,269 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -import asyncio -import logging -import zlib -from platform import system -from random import random -from typing import TYPE_CHECKING, Any - -from aiohttp import ( - ClientConnectionError, - ClientConnectorError, - ClientSession, - ClientWebSocketResponse, - WSMsgType, -) - -from ..errors import DisallowedIntents, InvalidAuth, ShardingRequired -from ..utils import dumps, loads -from .passthrough import PassThrough - -if TYPE_CHECKING: - from ..state import State - from .notifier import Notifier - -ZLIB_SUFFIX = b'\x00\x00\xff\xff' -url = '{base}/?v={version}&encoding=json&compress=zlib-stream' -_log = logging.getLogger(__name__) - - -RESUMABLE: list[int] = [ - 4000, - 4001, - 4002, - 4003, - 4005, - 4007, - 4008, - 4009, -] - - -class Shard: - def __init__( - self, - id: int, - state: State, - session: ClientSession, - notifier: Notifier, - version: int = 10, - ) -> None: - self.id = id - self.session_id: str | None = None - self.version = version - self._token: str | None = None - # while the rate limit normally is 120, it'd be nice to leave out - # 10 for heartbeating - self._rate_limiter = PassThrough(110, 60) - self._notifier = notifier - self._state = state - self._session = session - self._inflator: zlib._Decompress | None = None - self._sequence: int | None = None - self._ws: ClientWebSocketResponse | None = None - self._resume_gateway_url: str | None = None - self._heartbeat_interval: float | None = None - self._hb_received: asyncio.Future[None] | None = None - self._receive_task: asyncio.Task[None] | None = None - self._connection_alive: asyncio.Future[None] = asyncio.Future() - self._hello_received: asyncio.Future[None] | None = None - self._hb_task: asyncio.Task[None] | None = None - - async def connect(self, token: str | None = None, resume: bool = False) -> None: - self._hello_received = asyncio.Future() - self._inflator = zlib.decompressobj() - - try: - async with self._state.shard_concurrency: - _log.debug(f'shard:{self.id}: connecting to gateway') - self._ws = await self._session.ws_connect( - url=url.format(version=self.version, base=self._resume_gateway_url) - if resume and self._resume_gateway_url - else url.format( - version=self.version, base='wss://gateway.discord.gg' - ), - proxy=self._notifier.manager.proxy, - proxy_auth=self._notifier.manager.proxy_auth, - ) - _log.debug(f'shard:{self.id}: connected to gateway') - except (ClientConnectionError, ClientConnectorError): - _log.debug( - f'shard:{self.id}: failed to connect to discord due to connection errors, retrying in 10 seconds' - ) - await asyncio.sleep(10) - await self.connect(token=token, resume=resume) - return - else: - self._receive_task = asyncio.create_task(self._recv()) - - if token: - await self._hello_received - self._token = token - if resume: - await self.send_resume() - else: - await self.send_identify() - - async def send(self, data: dict[str, Any]) -> None: - async with self._rate_limiter: - d = dumps(data) - _log.debug(f'shard:{self.id}: sending {d}') - await self._ws.send_str(d) - - async def send_identify(self) -> None: - await self.send( - { - 'op': 2, - 'd': { - 'token': self._token, - 'properties': { - 'os': system(), - 'browser': 'pycord', - 'device': 'pycord', - }, - 'compress': True, - 'large_threshold': self._state.large_threshold, - 'shard': [self.id, self._notifier.manager.amount], - 'intents': self._state.intents.as_bit, - }, - } - ) - - async def send_resume(self) -> None: - await self.send( - { - 'op': 6, - 'd': { - 'token': self._token, - 'session_id': self.session_id, - 'seq': self._sequence, - }, - } - ) - - async def send_heartbeat(self, jitter: bool = False) -> None: - if jitter: - await asyncio.sleep(self._heartbeat_interval * random()) - else: - await asyncio.sleep(self._heartbeat_interval) - self._hb_received = asyncio.Future() - _log.debug(f'shard:{self.id}: sending heartbeat') - try: - await self._ws.send_str(dumps({'op': 1, 'd': self._sequence})) - except ConnectionResetError: - _log.debug( - f'shard:{self.id}: failed to send heartbeat due to connection reset, reconnecting...' - ) - self._receive_task.cancel() - if not self._ws.closed: - await self._ws.close(code=1008) - await self.connect(self._token, bool(self._resume_gateway_url)) - return - try: - await asyncio.wait_for(self._hb_received, 5) - except asyncio.TimeoutError: - _log.debug(f'shard:{self.id}: heartbeat waiting timed out, reconnecting...') - self._receive_task.cancel() - if not self._ws.closed: - await self._ws.close(code=1008) - await self.connect(self._token, bool(self._resume_gateway_url)) - - async def _recv(self) -> None: - async for msg in self._ws: - if msg.type == WSMsgType.CLOSED: - break - elif msg.type == WSMsgType.BINARY: - if len(msg.data) < 4 or msg.data[-4:] != ZLIB_SUFFIX: - continue - - try: - text_coded = self._inflator.decompress(msg.data).decode('utf-8') - except Exception as e: - # while being an edge case, the data could sometimes be corrupted. - _log.debug( - f'shard:{self.id}: failed to decompress gateway data {msg.data}:{e}' - ) - continue - - _log.debug(f'shard:{self.id}: received message {text_coded}') - - data: dict[str, Any] = loads(text_coded) - - self._sequence = data.get('s') - - op: int = data.get('op') - d: dict[str, Any] | int | None = data.get('d') - t: str | None = data.get('t') - - if op == 0: - if t == 'READY': - self.session_id = d['session_id'] - self._resume_gateway_url = d['resume_gateway_url'] - self._state.raw_user = d['user'] - asyncio.create_task(self._state.event_manager.publish(t, d)) - elif op == 1: - await self._ws.send_str(dumps({'op': 1, 'd': self._sequence})) - elif op == 10: - self._heartbeat_interval = d['heartbeat_interval'] / 1000 - - asyncio.create_task(self.send_heartbeat(jitter=True)) - self._hello_received.set_result(True) - elif op == 11: - if not self._hb_received.done(): - self._hb_received.set_result(None) - - self._hb_task = asyncio.create_task(self.send_heartbeat()) - elif op == 7: - await self._ws.close(code=1002) - await self.connect(token=self._token, resume=True) - return - elif op == 9: - await self._ws.close() - await self.connect(token=self._token) - return - await self.handle_close(self._ws.close_code) - - async def handle_close(self, code: int | None) -> None: - _log.debug(f'shard:{self.id}: closed with code {code}') - if self._hb_task and not self._hb_task.done(): - self._hb_task.cancel() - if code in RESUMABLE: - await self.connect(self._token, True) - elif code is None: - await self.connect(self._token, True) - else: - if code == 4004: - raise InvalidAuth('Authentication used in gateway is invalid') - elif code == 4011: - raise ShardingRequired('Discord is requiring you shard your bot') - elif code == 4014: - raise DisallowedIntents( - "You aren't allowed to carry a privileged intent wanted" - ) - - if code > 4000 or code == 4000: - await self._notifier.shard_died(self) - else: - # the connection most likely died - await self.connect(self._token, resume=True) diff --git a/pycord/guild.py b/pycord/guild.py index 67a48f7b..72dc5dae 100644 --- a/pycord/guild.py +++ b/pycord/guild.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,1018 +18,357 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE + from __future__ import annotations -import datetime -from typing import TYPE_CHECKING, Any +from datetime import datetime +from typing import Any, TYPE_CHECKING -from .auto_moderation import ( - AutoModAction, - AutoModEventType, - AutoModRule, - AutoModTriggerMetadata, - AutoModTriggerType, -) -from .channel import ( - CHANNEL_TYPE, - AnnouncementThread, - CategoryChannel, - Channel, - ForumTag, - TextChannel, - Thread, - VoiceChannel, - _Overwrite, - identify_channel, -) +from discord_typings._resources._guild import WelcomeChannelData, WelcomeScreenData + +from .asset import Asset +from .emoji import Emoji from .enums import ( - ChannelType, - DefaultMessageNotificationLevel, - ExplicitContentFilterLevel, - MFALevel, - NSFWLevel, - PremiumTier, - SortOrderType, + DefaultMessageNotificationLevel, ExplicitContentFilterLevel, MFALevel, NSFWLevel, PremiumTier, VerificationLevel, - VideoQualityMode, -) -from .file import File -from .flags import Permissions, SystemChannelFlags -from .media import Emoji, Sticker -from .member import Member, MemberPaginator -from .missing import MISSING, Maybe, MissingEnum -from .pages.paginator import Page, Paginator -from .role import Role -from .scheduled_event import ScheduledEvent -from .snowflake import Snowflake -from .types import ( - GUILD_FEATURE, - LOCALE, - Ban as DiscordBan, - Guild as DiscordGuild, - GuildPreview as DiscordGuildPreview, - UnavailableGuild, - Widget as DiscordWidget, - WidgetSettings as DiscordWidgetSettings, ) +from .flags import MemberFlags, Permissions, SystemChannelFlags, RoleFlags +from .missing import Maybe, MISSING +from .mixins import Identifiable +from .sticker import Sticker from .user import User -from .utils import remove_undefined -from .welcome_screen import WelcomeScreen if TYPE_CHECKING: - from .state import State - + from discord_typings import GuildData, PartialGuildData, UnavailableGuildData, GuildFeatures, RoleData, RoleTagsData, GuildMemberData -class ChannelPosition: - __slots__ = ('id', 'position', 'lock_permissions', 'parent_id') - - def __init__( - self, - id: Snowflake, - position: int, - *, - lock_permissions: bool = False, - parent_id: Snowflake | None | MissingEnum, - ) -> None: - self.id = id - self.position = position - self.lock_permissions = lock_permissions - self.parent_id = parent_id + from .state import State - def to_dict(self) -> dict[str, Any]: - payload = { - 'id': self.id, - 'position': self.position, - 'lock_permissions': self.lock_permissions, - 'parent_id': self.parent_id, - } - return remove_undefined(**payload) +__all__ = ( + "Guild", + "Role", + "RoleTags", + "GuildMember", + "WelcomeScreen", + "WelcomeScreenChannel", +) -class Guild: +class Guild(Identifiable): __slots__ = ( - '_state', - '_icon', - '_icon_hash', - '_splash', - '_discovery_splash', - '_afk_channel_id', - '_widget_channel_id', - '_roles', - '_emojis', - '_application_id', - '_system_channel_id', - '_rules_channel_id', - '_public_updates_channel_id', - '_banner', - '_welcome_screen', - 'id', - 'unavailable', - 'name', - 'owner', - 'owner_id', - 'permissions', - 'afk_channel_id', - 'afk_timeout', - 'widget_enabled', - 'widget_channel_id', - 'verification_level', - 'default_message_notifications', - 'explicit_content_filter', - 'roles', - 'emojis', - 'features', - 'mfa_level', - 'application_id', - 'system_channel_id', - 'rules_channel_id', - 'max_presences', - 'max_members', - 'vanity_url', - 'description', - 'premium_tier', - 'premium_subscription_count', - 'preferred_locale', - 'public_updates_channel_id', - 'max_video_channel_users', - 'approximate_member_count', - 'approximate_presence_count', - 'welcome_screen', - 'nsfw_level', - 'stickers', - 'premium_progress_bar_enabled', - 'system_channel_flags', + "_state", + "_update", + "id", + "name", + "icon_hash", + "splash_hash", + "discovery_splash_hash", + "owner", + "owner_id", + "permissions", + "afk_channel_id", + "afk_timeout", + "widget_enabled", + "widget_channel_id", + "verification_level", + "default_message_notifications", + "explicit_content_filter", + "roles", + "emojis", + "features", + "mfa_level", + "application_id", + "system_channel_id", + "system_channel_flags", + "rules_channel_id", + "max_presences", + "max_members", + "vanity_url_code", + "description", + "banner_hash", + "premium_tier", + "premium_subscription_count", + "preferred_locale", + "public_updates_channel_id", + "max_video_channel_users", + "max_stage_video_channel_users", + "approximate_member_count", + "approximate_presence_count", + "welcome_screen", + "nsfw_level", + "stickers", + "premium_progress_bar_enabled", + "safety_alerts_channel_id" ) - def __init__(self, data: DiscordGuild | UnavailableGuild, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self._state = state - if not data.get('unavailable'): - self.unavailable: bool = False - self.name: str = data['name'] - # TODO: Asset classes - self._icon: str | None = data['icon'] - self._icon_hash: str | None | MissingEnum = data.get('icon_hash') - self._splash: str | None = data['splash'] - self._discovery_splash: str | None = data['discovery_splash'] - self.owner: bool | MissingEnum = data.get('owner', MISSING) - self.owner_id: Snowflake = Snowflake(data.get('owner_id')) - self.permissions: Permissions = Permissions.from_value( - data.get('permissions', 0) - ) - self._afk_channel_id: str | None = data.get('afk_channel_id') - self.afk_channel_id: Snowflake | None = ( - Snowflake(self._afk_channel_id) - if self._afk_channel_id is not None - else None - ) - self.afk_timeout: int = data.get('afk_timeout') - self.widget_enabled: bool | MissingEnum = data.get( - 'widget_enabled', MISSING - ) - self._widget_channel_id: str | None | MissingEnum = data.get( - 'widget_channel_id' - ) - self.widget_channel_id: MissingEnum | Snowflake | None = ( - Snowflake(self._widget_channel_id) - if isinstance(self._widget_channel_id, str) - else self._widget_channel_id - ) - self.verification_level: VerificationLevel = VerificationLevel( - data['verification_level'] - ) - self.default_message_notifications: DefaultMessageNotificationLevel = ( - DefaultMessageNotificationLevel(data['default_message_notifications']) - ) - self.explicit_content_filter: ExplicitContentFilterLevel = ( - ExplicitContentFilterLevel(data['explicit_content_filter']) - ) - self._roles: list[dict[str, Any]] = data.get('roles', []) - self._process_roles() - self._emojis: list[dict[str, Any]] = data.get('emojis', []) - self._process_emojis() - self.features: list[GUILD_FEATURE] = data['features'] - self.mfa_level: MFALevel = MFALevel(data['mfa_level']) - self._application_id: str | None = data.get('application_id') - self.application_id: Snowflake | None = ( - Snowflake(self._application_id) - if self._application_id is not None - else None - ) - self._system_channel_id: str | None = data.get('system_channel_id') - self.system_channel_id: Snowflake | None = ( - Snowflake(self._system_channel_id) - if self._system_channel_id is not None - else None - ) - self.system_channel_flags: SystemChannelFlags = ( - SystemChannelFlags.from_value(data['system_channel_flags']) - ) - - self._rules_channel_id: str | None = data.get('rules_channel_id') - self.rules_channel_id: Snowflake | None = ( - Snowflake(self._rules_channel_id) - if self._rules_channel_id is not None - else None - ) - self.max_presences: int | MissingEnum = data.get('max_presences', MISSING) - self.max_members: int | MissingEnum = data.get('max_members', MISSING) - self.vanity_url: str | None = data.get('vanity_url_code') - self.description: str | None = data.get('description') - self._banner: str | None = data.get('banner') - self.premium_tier: PremiumTier = PremiumTier(data['premium_tier']) - self.premium_subscription_count: int | MissingEnum = data.get( - 'premium_subscription_count', MISSING - ) - self.preferred_locale: LOCALE = data['preferred_locale'] - self._public_updates_channel_id: str | None = data[ - 'public_updates_channel_id' - ] - self.public_updates_channel_id: Snowflake | None = ( - Snowflake(self._public_updates_channel_id) - if self._public_updates_channel_id is not None - else None - ) - self.max_video_channel_users: int | MissingEnum = data.get( - 'max_video_channel_users', MISSING - ) - self.approximate_member_count: int | MissingEnum = data.get( - 'approximate_member_count', MISSING - ) - self.approximate_presence_count: int | MissingEnum = data.get( - 'approximate_presence_count', MISSING - ) - self._welcome_screen = data.get('welcome_screen', MISSING) - self.welcome_screen: WelcomeScreen | MissingEnum = ( - WelcomeScreen(self._welcome_screen) - if self._welcome_screen is not MISSING - else MISSING - ) - self.nsfw_level: NSFWLevel = NSFWLevel(data.get('nsfw_level', 0)) - self.stickers: list[Sticker] = [ - Sticker(d, self._state) for d in data.get('stickers', []) - ] - self.premium_progress_bar_enabled: bool = data[ - 'premium_progress_bar_enabled' - ] - else: - self.unavailable: bool = True - - def _process_roles(self) -> None: - self.roles: list[Role] = [Role(role, state=self._state) for role in self._roles] - - def _process_emojis(self) -> None: - self.emojis: list[Emoji] = [] - for emoji in self._emojis: - emo = Emoji(emoji, state=self._state) - emo._inject_roles(self.roles) - self.emojis.append(emo) - - async def list_auto_moderation_rules(self) -> list[AutoModRule]: - """list the auto moderation rules for this guild. - - Returns - ------- - list[:class:`AutoModRule`] - The auto moderation rules for this guild. - """ - data = await self._state.http.list_auto_moderation_rules_for_guild(self.id) - return [AutoModRule(rule, self._state) for rule in data] - - async def get_auto_moderation_rule(self, rule_id: int) -> AutoModRule: - """Get an auto moderation rule for this guild. - - Parameters - ---------- - rule_id: :class:`int` - The ID of the rule to get. - - Returns - ------- - :class:`AutoModRule` - The auto moderation rule for this guild. - """ - data = await self._state.http.get_auto_moderation_rule_for_guild( - self.id, rule_id - ) - return AutoModRule(data, self._state) - - async def create_auto_moderation_rule( - self, - *, - name: str, - event_type: AutoModEventType, - trigger_type: AutoModTriggerType, - trigger_metadata: AutoModTriggerMetadata | MissingEnum = MISSING, - actions: list[AutoModAction], - enabled: bool = False, - exempt_roles: list[Snowflake] | MissingEnum = MISSING, - exempt_channels: list[Snowflake] | MissingEnum = MISSING, - reason: str | None = None, - ) -> AutoModRule: - """Create an auto moderation rule for this guild. - - Parameters - ---------- - name: :class:`str` - The name of the rule. - event_type: :class:`AutoModEventType` - The event type of the rule. - trigger_type: :class:`AutoModTriggerType` - The trigger type of the rule. - trigger_metadata: :class:`AutoModTriggerMetadata` - The trigger metadata of the rule. - actions: list[:class:`AutoModAction`] - The actions to take when the rule is triggered. - enabled: :class:`bool` - Whether the rule is enabled. - exempt_roles: list[:class:`Snowflake`] - The roles to exempt from this rule. - exempt_channels: list[:class:`Snowflake`] - The channels to exempt from this rule. - reason: :class:`str` | None - The reason for creating the rule. - - Returns - ------- - :class:`AutoModRule` - The auto moderation rule for this guild. - """ - data = await self._state.http.create_auto_moderation_rule_for_guild( - self.id, - name=name, - event_type=event_type, - trigger_type=trigger_type, - trigger_metadata=trigger_metadata, - actions=actions, - enabled=enabled, - exempt_roles=exempt_roles, - exempt_channels=exempt_channels, - reason=reason, - ) - return AutoModRule(data, self._state) - - async def create_emoji( - self, - *, - name: str, - image: bytes, # TODO - roles: list[Role] | None = None, - reason: str | None = None, - ) -> Emoji: - """Creates an emoji. - - Parameters - ---------- - name: :class:`str` - The name of the emoji. - image: :class:`bytes` - The image data of the emoji. - roles: list[:class:`Role`] - The roles that can use the emoji. - reason: :class:`str` | None - The reason for creating the emoji. Shows up on the audit log. - - Returns - ------- - :class:`Emoji` - The created emoji. - """ - data = await self._state.http.create_guild_emoji( - self.id, name, image, roles, reason - ) - return Emoji(data, state=self._state) - - async def edit_emoji( - self, - emoji_id: Snowflake, - *, - name: str | MissingEnum = MISSING, - roles: list[Role] | MissingEnum = MISSING, - reason: str | None = None, - ) -> Emoji: - """Edits the emoji. - - Parameters - ---------- - emoji_id: :class:`Snowflake` - The ID of the emoji to edit. - name: :class:`str` - The new name of the emoji. - roles: list[:class:`Role`] - The new roles that can use the emoji. - reason: :class:`str` | None - The reason for editing the emoji. Shows up on the audit log. - - Returns - ------- - :class:`Emoji` - The edited emoji. - """ - data = await self._state.http.modify_guild_emoji( - self.id, emoji_id, name=name, roles=roles, reason=reason - ) - return Emoji(data, self._state) - - async def delete_emoji( - self, emoji_id: Snowflake, *, reason: str | None = None - ) -> None: - """Deletes an emoji. - - Parameters - ---------- - emoji_id: :class:`Snowflake` - The ID of the emoji to delete. - reason: :class:`str` | None - The reason for deleting the emoji. Shows up on the audit log. - """ - await self._state.http.delete_guild_emoji(self.id, emoji_id, reason=reason) - - async def get_preview(self) -> GuildPreview: - """Get a preview of this guild. - - Returns - ------- - :class:`GuildPreview` - The preview of this guild. - """ - data = await self._state.http.get_guild_preview(self.id) - return GuildPreview(data, self._state) - - async def edit( - self, - *, - name: str | MissingEnum = MISSING, - verification_level: VerificationLevel | None | MissingEnum = MISSING, - default_message_notifications: DefaultMessageNotificationLevel - | None - | MissingEnum = MISSING, - explicit_content_filter: ExplicitContentFilterLevel - | None - | MissingEnum = MISSING, - afk_channel: VoiceChannel | None | MissingEnum = MISSING, - afk_timeout: int | MissingEnum = MISSING, - icon: File | None | MissingEnum = MISSING, - owner: User | MissingEnum = MISSING, - splash: File | None | MissingEnum = MISSING, - discovery_splash: File | None | MissingEnum = MISSING, - banner: File | None | MissingEnum = MISSING, - system_channel: TextChannel | None | MissingEnum = MISSING, - rules_channel: TextChannel | None | MissingEnum = MISSING, - public_updates_channel: TextChannel | None | MissingEnum = MISSING, - preferred_locale: str | None | MissingEnum = MISSING, - features: list[GUILD_FEATURE] | MissingEnum = MISSING, - description: str | None | MissingEnum = MISSING, - premium_progress_bar_enabled: bool | MissingEnum = MISSING, - reason: str | None = None, - ) -> Guild: - """Edits the guild. - - Parameters - ---------- - name: :class:`str` - The new name of the guild. - verification_level: :class:`VerificationLevel` - The new verification level of the guild. - default_message_notifications: :class:`DefaultMessageNotificationLevel` - The new default message notification level of the guild. - explicit_content_filter: :class:`ExplicitContentFilterLevel` - The new explicit content filter level of the guild. - afk_channel: :class:`VoiceChannel` - The new AFK channel of the guild. - afk_timeout: :class:`int` - The new AFK timeout of the guild. - icon: :class:`.File` - The new icon of the guild. - owner: :class:`User` - The new owner of the guild. - splash: :class:`.File` - The new splash of the guild. - discovery_splash: :class:`.File` - The new discovery splash of the guild. - banner: :class:`.File` - The new banner of the guild. - system_channel: :class:`TextChannel` - The new system channel of the guild. - rules_channel: :class:`TextChannel` - The new rules channel of the guild. - public_updates_channel: :class:`TextChannel` - The new public updates channel of the guild. - preferred_locale: :class:`str` - The new preferred locale of the guild. - features: list[:class:`GUILD_FEATURE`] - The new features of the guild. - description: :class:`str` - The new description of the guild. - reason: :class:`str` | None - The reason for editing the guild. Shows up on the audit log. - premium_progress_bar_enabled: :class:`bool` - Whether the premium progress bar is enabled. - - Returns - ------- - :class:`Guild` - The edited guild. - """ - data = await self._state.http.modify_guild( - self.id, - name=name, - verification_level=verification_level.value - if verification_level - else verification_level, - default_message_notifications=default_message_notifications.value - if default_message_notifications - else default_message_notifications, - explicit_content_filter=explicit_content_filter.value - if explicit_content_filter - else explicit_content_filter, - afk_channel_id=afk_channel.id if afk_channel else afk_channel, - afk_timeout=afk_timeout, - icon=icon, - owner_id=owner.id if owner else owner, - splash=splash, - discovery_splash=discovery_splash, - banner=banner, - system_channel_id=system_channel.id if system_channel else system_channel, - rules_channel_id=rules_channel.id if rules_channel else rules_channel, - public_updates_channel_id=public_updates_channel.id - if public_updates_channel - else public_updates_channel, - preferred_locale=preferred_locale, - features=features, - description=description, - premium_progress_bar_enabled=premium_progress_bar_enabled, - reason=reason, - ) - return Guild(data, self._state) - - async def delete(self) -> None: - """Deletes the guild.""" - await self._state.http.delete_guild(self.id) - - async def get_channels(self) -> list[CHANNEL_TYPE]: - """Gets the channels of the guild. - - Returns - ------- - list[:class:`Channel`] - The channels of the guild. - """ - data = await self._state.http.get_guild_channels(self.id) - return [identify_channel(channel, self._state) for channel in data] - - async def create_channel( - self, - name: str, - type: ChannelType, - *, - topic: str | None | MissingEnum = MISSING, - bitrate: int | None | MissingEnum = MISSING, - user_limit: int | None | MissingEnum = MISSING, - rate_limit_per_user: int | None | MissingEnum = MISSING, - position: int | None | MissingEnum = MISSING, - permission_overwrites: list[_Overwrite] | None | MissingEnum = MISSING, - parent: CategoryChannel | None | MissingEnum = MISSING, - nsfw: bool | MissingEnum = MISSING, - rtc_region: str | None | MissingEnum = MISSING, - video_quality_mode: VideoQualityMode | None | MissingEnum = MISSING, - default_auto_archive_duration: int | None | MissingEnum = MISSING, - default_reaction_emoji: str | None | MissingEnum = MISSING, - available_tags: list[ForumTag] | None | MissingEnum = MISSING, - default_sort_order: SortOrderType | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> CHANNEL_TYPE: - """Creates a channel in the guild. - - Parameters - ---------- - name: :class:`str` - The name of the channel. - type: :class:`ChannelType` - The type of the channel. - topic: :class:`str` | None | :class:`.MissingEnum` - The topic of the channel. - bitrate: :class:`int` | None | :class:`.MissingEnum` - The bitrate of the channel. - user_limit: :class:`int` | None | :class:`.MissingEnum` - The user limit of the channel. - rate_limit_per_user: :class:`int` | None | :class:`.MissingEnum` - The rate limit per user of the channel. - position: :class:`int` | None | :class:`.MissingEnum` - The position of the channel. - permission_overwrites: list[:class:`_Overwrite`] - The permission overwrites of the channel. - parent: :class:`CategoryChannel` | None | :class:`.MissingEnum` - The parent of the channel. - nsfw: :class:`bool` | :class:`.MissingEnum` - Whether the channel is NSFW. - rtc_region: :class:`str` | None | :class:`.MissingEnum` - The RTC region of the channel. - video_quality_mode: :class:`VideoQualityMode` | None | :class:`.MissingEnum` - The video quality mode of the channel. - default_auto_archive_duration: :class:`int` | None | :class:`.MissingEnum` - The default auto archive duration of the channel. - default_reaction_emoji: :class:`str` | None | :class:`.MissingEnum` - The default reaction emoji of the channel. - available_tags: list[:class:`ForumTag`] | None | :class:`.MissingEnum` - The available tags of the channel. - default_sort_order: :class:`SortOrderType` | None | :class:`.MissingEnum` - The default sort order of the channel. - reason: :class:`str` | None - The reason for creating the channel. Shows up on the audit log. - - Returns - ------- - :class:`Channel` - The created channel. - """ - data = await self._state.http.create_channel( - self.id, - name=name, - type=type.value if type else type, - topic=topic, - bitrate=bitrate, - user_limit=user_limit, - rate_limit_per_user=rate_limit_per_user, - position=position, - permission_overwrites=[o.to_dict() for o in permission_overwrites], - parent_id=parent.id if parent else parent, - nsfw=nsfw, - rtc_region=rtc_region, - video_quality_mode=video_quality_mode, - default_auto_archive_duration=default_auto_archive_duration, - default_reaction_emoji=default_reaction_emoji, - available_tags=available_tags, - default_sort_order=default_sort_order, - reason=reason, - ) - return identify_channel(data, self._state) - - async def modify_channel_positions(self, channels: list[ChannelPosition]) -> None: - """Modifies the positions of the channels. - - Parameters - ---------- - channels: list[:class:`ChannelPosition`] - The channels to modify. - """ - await self._state.http.modify_channel_positions( - self.id, [c.to_dict() for c in channels] - ) - - async def list_active_threads(self) -> list[Thread | AnnouncementThread]: - """Lists the active threads in the guild. - - Returns - ------- - list[:class:`Channel`] - The active threads in the guild. - """ - data = await self._state.http.list_active_threads(self.id) - return [identify_channel(channel, self._state) for channel in data] - - async def get_member(self, id: Snowflake): - """Gets a member from the guild. - - Parameters - ---------- - id: :class:`Snowflake` - The ID of the member. - - Returns - ------- - :class:`Member` - The member. - """ - data = await self._state.http.get_member(self.id, id) - return Member(data, self._state, guild_id=self.id) - - def list_members( - self, limit: int = None, after: datetime.datetime | None = None - ) -> MemberPaginator: - """Lists the members in the guild. - - Parameters - ---------- - limit: :class:`int` - The maximum number of members to return. - after: :class:`datetime.datetime` | None - List only members whos accounts were created after this date. - - Returns - ------- - :class:`MemberPaginator` - An async iterator that can be used for iterating over the guild's members. - """ - return MemberPaginator(self._state, self.id, limit=limit, after=after) - - async def search_members( - self, - query: str, - *, - limit: int = None, - ) -> list[Member]: - """Searches for members in the guild. - - Parameters - ---------- - query: :class:`str` - The query to search for. - limit: :class:`int` - The maximum number of members to return. - - Returns - ------- - list[:class:`Member`] - The members. - """ - data = await self._state.http.search_guild_members(self.id, query, limit=limit) - return [Member(member, self._state, guild_id=self.id) for member in data] - - async def add_member( - self, - id: Snowflake, - access_token: str, - *, - nick: str | None = None, - roles: list[Role] | None = None, - mute: bool = False, - deaf: bool = False, - ) -> Member: - """Adds a member to the guild through Oauth2. - - Parameters - ---------- - id: :class:`Snowflake` - The ID of the member. - access_token: :class:`str` - The access token of the member. - nick: :class:`str` | None - The nickname of the member. - roles: list[:class:`Role`] | None - The roles of the member. - mute: :class:`bool` - Whether the member is muted. - deaf: :class:`bool` - Whether the member is deafened. - - Returns - ------- - :class:`Member` - The member. - """ - nick = nick or MISSING - roles = roles or [] - data = await self._state.http.add_member( - self.id, - id, - access_token, - nick=nick, - roles=[role.id for role in roles], - mute=mute, - deaf=deaf, - ) - return Member(data, self._state, guild_id=self.id) - - async def edit_own_member( - self, *, nick: str | None | MissingEnum = MISSING, reason: str | None = None - ) -> Member: - """Edits the bot's guild member. - - Parameters - ---------- - nick: :class:`str` | None | :class:`MissingEnum` - The bot's new nickname. - reason: :class:`str` | None - The reasoning for editing the bot. Shows up in the audit log. - - Returns - ------- - :class:`Member` - The updated member. - """ - data = await self._state.http.modify_current_member( - self.id, nick=nick, reason=reason - ) - return Member(data, self._state, guild_id=self.id) - - def get_bans( - self, - *, - limit: int | None = 1000, - before: datetime.datetime | None = None, - after: datetime.datetime | None = None, - ) -> BanPaginator: - """Lists the bans in the guild. - - .. note:: - If both ``after`` and ``before`` parameters are provided, - only ``before`` will be respected. + def __init__(self, data: GuildData | PartialGuildData | UnavailableGuildData, state: State) -> None: + self._state: State = state + self._update(data) + + def __repr__(self) -> str: + return super().__repr__() + + def __str__(self) -> str: + return str(self.name) + + def _update(self, data: GuildData | PartialGuildData | UnavailableGuildData) -> None: + self.id: int = int(data["id"]) + + self.name: Maybe[str] = data.get("name", MISSING) + self.icon_hash: Maybe[str | None] = data.get("icon", MISSING) + self.splash_hash: Maybe[str | None] = data.get("splash", MISSING) + self.discovery_splash_hash: Maybe[str | None] = data.get("discovery_splash", MISSING) + self.owner: Maybe[bool] = data.get("owner", MISSING) + self.owner_id: Maybe[int] = int(oid) if (oid := data.get("owner_id")) else MISSING + self.permissions: Maybe[Permissions] = Permissions.from_value(permdata) if ( + permdata := data.get("permissions", MISSING)) else MISSING + self.afk_channel_id: Maybe[int] = int(afkid) if (afkid := data.get("afk_channel_id")) else MISSING + self.afk_timeout: Maybe[int] = data.get("afk_timeout", MISSING) + self.widget_enabled: Maybe[bool] = data.get("widget_enabled", MISSING) + self.widget_channel_id: Maybe[int] = int(widgetid) if (widgetid := data.get("widget_channel_id")) else MISSING + self.verification_level: Maybe[VerificationLevel] = VerificationLevel(verlvl) if ( + verlvl := data.get("verification_level")) else MISSING + self.default_message_notifications: Maybe[DefaultMessageNotificationLevel] = DefaultMessageNotificationLevel( + dmn + ) if (dmn := data.get("default_message_notifications")) else MISSING + self.explicit_content_filter: Maybe[ExplicitContentFilterLevel] = ExplicitContentFilterLevel(ecf) if ( + ecf := data.get("explicit_content_filter")) else MISSING + self.roles: Maybe[list[Role]] = [Role(role, self._state) for role in roles] if ( + roles := data.get("roles")) else MISSING + self.emojis: Maybe[list[Emoji]] = [Emoji(emoji, self._state) for emoji in emojis] if ( + emojis := data.get("emojis")) else MISSING + self.features: Maybe[list[GuildFeatures]] = data.get("features", MISSING) + self.mfa_level: Maybe[MFALevel] = MFALevel(mfa) if (mfa := data.get("mfa_level")) else MISSING + self.application_id: int | None = int(appid) if (appid := data.get("application_id")) else None + self.system_channel_id: int | None = int(sysid) if (sysid := data.get("system_channel_id")) else None + self.system_channel_flags: Maybe[SystemChannelFlags] = SystemChannelFlags.from_value(sysflags) if ( + sysflags := data.get("system_channel_flags", MISSING)) else MISSING + self.rules_channel_id: int | None = int(rulesid) if (rulesid := data.get("rules_channel_id")) else None + self.max_presences: Maybe[int | None] = data.get("max_presences", MISSING) + self.max_members: Maybe[int | None] = data.get("max_members", MISSING) + self.vanity_url_code: Maybe[str | None] = data.get("vanity_url_code", MISSING) + self.description: Maybe[str | None] = data.get("description", MISSING) + self.banner_hash: Maybe[str | None] = data.get("banner", MISSING) + self.premium_tier: Maybe[PremiumTier] = PremiumTier(pt) if (pt := data.get("premium_tier")) else MISSING + self.premium_subscription_count: Maybe[int] = data.get("premium_subscription_count", MISSING) + self.preferred_locale: Maybe[str] = data.get("preferred_locale", MISSING) + self.public_updates_channel_id: int | None = int(pubupid) if ( + pubupid := data.get("public_updates_channel_id")) else None + self.max_video_channel_users: Maybe[int] = data.get("max_video_channel_users", MISSING) + self.max_stage_video_channel_users: Maybe[int] = data.get("max_stage_video_channel_users", MISSING) + self.approximate_member_count: Maybe[int] = data.get("approximate_member_count", MISSING) + self.approximate_presence_count: Maybe[int] = data.get("approximate_presence_count", MISSING) + self.welcome_screen: Maybe[WelcomeScreen] = WelcomeScreen(wlc) if ( + wlc := data.get("welcome_screen")) else MISSING + self.nsfw_level: Maybe[NSFWLevel] = NSFWLevel(nsfw) if (nsfw := data.get("nsfw_level")) else MISSING + self.stickers: Maybe[list[Sticker]] = [Sticker(sticker, self._state) for sticker in stickers] if ( + stickers := data.get("stickers")) else MISSING + self.premium_progress_bar_enabled: Maybe[bool] = data.get("premium_progress_bar_enabled", MISSING) + self.safety_alerts_channel_id: int | None = int(safetyid) if ( + safetyid := data.get("safety_alerts_channel_id")) else None + + @property + def icon(self) -> Asset | None: + return Asset.from_guild_icon(self._state, self.id, self.icon_hash) if self.icon_hash else None + + @property + def splash(self) -> Asset | None: + return Asset.from_guild_splash(self._state, self.id, self.splash_hash) if self.splash_hash else None + + @property + def discovery_splash(self) -> Asset | None: + return Asset.from_guild_discovery_splash( + self._state, self.id, self.discovery_splash_hash + ) if self.discovery_splash_hash else None + + @property + def banner(self) -> Asset | None: + return Asset.from_guild_banner(self._state, self.id, self.banner_hash) if self.banner_hash else None + + +class Role(Identifiable): + __slots__ = ( + "_state", + "id", + "name", + "color", + "hoist", + "icon_hash", + "unicode_emoji", + "position", + "permissions", + "managed", + "mentionable", + "tags", + "flags" + ) - Parameters - ---------- - limit: :class:`int` - The maximum number of bans to return. - before: :class:`datetime.datetime` | None - List only bans related to users whos accounts - were created before this date. - This is not related to the ban's creation date. - after: :class:`datetime.datetime` | None - List only bans related to users whos accounts - were created after this date. - This is not related to the ban's creation date. + def __init__(self, data: RoleData, state: State) -> None: + self._state: State = state + self._update(data) + + def __repr__(self) -> str: + return super().__repr__() + + def __str__(self) -> str: + return self.name + + def _update(self, data: RoleData) -> None: + self.id: int = int(data["id"]) + self.name: str = data["name"] + self.color: int = data["color"] + self.hoist: bool = data["hoist"] + self.icon_hash: Maybe[str | None] = data.get("icon", MISSING) + self.unicode_emoji: Maybe[str | None] = data.get("unicode_emoji", MISSING) + self.position: int = data["position"] + self.permissions: Permissions = Permissions.from_value(data["permissions"]) + self.managed: bool = data["managed"] + self.mentionable: bool = data["mentionable"] + self.tags: Maybe[RoleTags] = RoleTags(roletags) if (roletags := data.get("tags")) else MISSING + self.flags: RoleFlags = RoleFlags.from_value(data["flags"]) + + @property + def icon(self) -> Asset | None: + return Asset.from_role_icon(self._state, self.id, self.icon_hash) if self.icon_hash else None + + +class RoleTags: + __slots__ = ( + "bot_id", + "integration_id", + "premium_subscriber", + "subscription_listing_id", + "available_for_purchase", + "guild_connections" + ) - Returns - ------- - :class:`BanPaginator` - An async iterator that can be used for iterating over the guild's bans. - """ - return BanPaginator( - self._state, self.id, limit=limit, before=before, after=after - ) + def __init__(self, data: RoleTagsData) -> None: + self.bot_id: Maybe[int] = int(botid) if (botid := data.get("bot_id")) else MISSING + self.integration_id: Maybe[int] = int(integrationid) if ( + integrationid := data.get("integration_id")) else MISSING + self.premium_subscriber: bool = "premium_subscriber" in data + self.subscription_listing_id: Maybe[int] = int(subid) if ( + subid := data.get("subscription_listing_id")) else MISSING + self.available_for_purchase: bool = "available_for_purchase" in data + self.guild_connections: bool = "guild_connections" in data - async def get_ban( - self, - user_id: Snowflake, - ) -> Ban: - """Gets a ban for a user. - Parameters - ---------- - user_id: :class:`Snowflake` - The user ID to fetch a ban for. +class GuildMember(Identifiable): + __slots__ = ( + "_state", + "_user", + "guild_id", + "nick", + "guild_avatar_hash", + "role_ids", + "joined_at", + "premium_since", + "deaf", + "mute", + "pending", + "permissions", + "communication_disabled_until" + ) - Returns - ------- - :class:`Ban` - The user's ban. - """ - data = await self._state.http.get_guild_ban(self.id, user_id) - return Ban(data, self._state) + def __init__(self, data: GuildMemberData, state: State, guild_id: int = None) -> None: + self._state: State = state + self.user: Maybe[User] = User(data["user"], self._state) if "user" in data else MISSING + self._update(data, guild_id) - async def ban( - self, - user: User, - *, - delete_message_seconds: int | MissingEnum = MISSING, - reason: str | None = None, - ) -> None: - """Bans a user. + def __repr__(self) -> str: + return f"" - Parameters - ---------- - user: :class:`User` - The user to ban - delete_message_seconds: :class:`int` | MissingEnum - The amount of seconds worth of messages that should be deleted. - reason: :class:`str` | None - The reason for the ban. Shows up in the audit log, and when the ban is fetched. - """ - await self._state.http.create_guild_ban( - self.id, - user.id, - delete_message_seconds=delete_message_seconds, - reason=reason, - ) + def __str__(self) -> str: + return self.display_name - async def unban(self, user: User, *, reason: str | None = None) -> None: - """Unbans a user. + def _update(self, data: GuildMemberData, guild_id: int) -> None: + self.guild_id: int = guild_id + if "user" in data: + self.id: int = int(data["user"]["id"]) + if self.user: + self.user._update(data["user"]) + else: + self.user: Maybe[User] = User(data["user"], self._state) + self.nick: Maybe[str | None] = data.get("nick", MISSING) + self.guild_avatar_hash: Maybe[str | None] = data.get("avatar", MISSING) + self.role_ids: list[int] = [int(role) for role in data.get("roles", [])] + self.joined_at: datetime = datetime.fromisoformat(data["joined_at"]) + self.premium_since: Maybe[datetime] = datetime.fromisoformat(ps) if \ + (ps := data.get("premium_since", MISSING)) else ps + self.deaf: bool = data["deaf"] + self.mute: bool = data["mute"] + self.flags: MemberFlags = MemberFlags.from_value(data["flags"]) + self.pending: Maybe[bool] = data.get("pending", MISSING) + self.permissions: Maybe[Permissions] = Permissions.from_value(perm) \ + if (perm := data.get("permissions", MISSING)) else perm + self.communication_disabled_until: Maybe[datetime | None] = datetime.fromisoformat(cd) if \ + (cd := data.get("communication_disabled_until", MISSING)) else cd + + @property + def guild_avatar(self) -> Asset | None: + return Asset.from_guild_member_avatar( + self._state, self.guild_id, self._user.id, self.guild_avatar_hash + ) if self.guild_avatar_hash else None + + @property + def display_avatar(self) -> Asset | None: + return self.guild_avatar or self.user.display_avatar + + @property + def display_name(self) -> str: + return self.nick or self.user.display_name + + @property + def mention(self) -> str: + return self.user.mention + + +class WelcomeScreen: + __slots__ = ( + "description", + "welcome_channels" + ) - Parameters - ---------- - user: :class:`User` - The user to unban - reason: :class:`str` | None - The reason for the unban. Shows up in the audit log. - """ - await self._state.http.remove_guild_ban(self.id, user.id, reason=reason) + def __init__(self, data: WelcomeScreenData) -> None: + self.description: str | None = data.get("description") + self.welcome_channels: list[WelcomeScreenChannel] = [ + WelcomeScreenChannel.from_dict(channel) for channel in data.get("welcome_channels", []) + ] - async def get_scheduled_events(self, with_user_count: bool) -> list[ScheduledEvent]: - """ - Get the scheduled events in this guild. - Parameters - ---------- - with_user_count: :class:`bool` - include number of users subscribed to each event +class WelcomeScreenChannel: + __slots__ = ( + "channel_id", + "description", + "emoji_id", + "emoji_name" + ) - Returns - ------- - :class:`list`[:class:`.ScheduledEvent`] - """ - scheds = await self._state.http.list_scheduled_events( - self.id, with_user_count=with_user_count + def __init__(self, channel_id: int, description: str, emoji_id: int | None, emoji_name: str | None) -> None: + self.channel_id: int = channel_id + self.description: str = description + self.emoji_id: int | None = emoji_id + self.emoji_name: str | None = emoji_name + + @classmethod + def from_dict(cls, data: WelcomeChannelData) -> WelcomeScreenChannel: + return cls( + data["channel_id"], + data["description"], + data.get("emoji_id"), + data.get("emoji_name") ) - return [ScheduledEvent(s, self._state) for s in scheds] - - -class GuildPreview: - def __init__(self, data: DiscordGuildPreview, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - # TODO: Asset classes - self._icon: str | None = data['icon'] - self._splash: str | None = data['splash'] - self._discovery_splash: str | None = data['discovery_splash'] - self.emojis: list[Emoji] = [Emoji(emoji, state) for emoji in data['emojis']] - self.features: list[GUILD_FEATURE] = data['features'] - self.approximate_member_count: int = data['approximate_member_count'] - self.approximate_presence_count: int = data['approximate_presence_count'] - self.description: str | None = data['description'] - self.stickers: list[Sticker] = [ - Sticker(sticker, state) for sticker in data['stickers'] - ] + def to_dict(self) -> WelcomeChannelData: + return { + "channel_id": self.channel_id, + "description": self.description, + "emoji_id": self.emoji_id, + "emoji_name": self.emoji_name + } -class WidgetSettings: - def __init__(self, data: DiscordWidgetSettings) -> None: - self.enabled: bool = data['enabled'] - self.channel_id: Snowflake | None = ( - Snowflake(data['channel_id']) if data['channel_id'] is not None else None + def __repr__(self) -> str: + return ( + f"" ) - - -class Widget: - def __init__(self, data: DiscordWidget, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.instant_invite: str | None = data['instant_invite'] - self.channels: list[Channel] = [ - Channel(channel, state) for channel in data['channels'] - ] - self.members: list[User] = [User(user, state) for user in data['members']] - - -class Ban: - def __init__(self, data: DiscordBan, state: State) -> None: - self.user: User = User(data['user'], state) - self.reason: str | None = data['reason'] - - -class BanPage(Page[Ban]): - def __init__(self, ban: Ban) -> None: - self.value = ban - - -class BanPaginator(Paginator[BanPage]): - def __init__( - self, - state: State, - guild_id: Snowflake, - *, - limit: int = 1, - before: datetime.datetime | None = None, - after: datetime.datetime | None = None, - ) -> None: - super().__init__() - self._state: State = state - self.guild_id: Snowflake = guild_id - self.limit: int | None = limit - self.reverse_order: bool = False - self.last_id: Snowflake | MissingEnum - if before: - self.last_id = Snowflake.from_datetime(before) - self.reverse_order = True - elif after: - self.last_id = Snowflake.from_datetime(after) - else: - self.last_id = MISSING - self.done = False - - async def fill(self): - if self._previous_page is None or self._previous_page[0] >= len(self._pages): - if self.done: - raise StopAsyncIteration - limit = min(self.limit, 1000) if self.limit else 1000 - if self.limit is not None: - self.limit -= limit - if self.reverse_order: - param = {'before': self.last_id} - else: - param = {'after': self.last_id} - data = await self._state.http.get_guild_bans( - self.guild_id, - limit=limit, - **param, - ) - if len(data) < limit or self.limit <= 0: - self.done = True - if not data: - raise StopAsyncIteration - if self.reverse_order: - data.reverse() - for member in data: - self.add_page( - BanPage( - Ban( - member, - self._state, - ) - ) - ) - - async def forward(self): - await self.fill() - value = await super().forward() - self.last_id = value.user.id - return value diff --git a/pycord/guild_scheduled_event.py b/pycord/guild_scheduled_event.py new file mode 100644 index 00000000..fbf329dc --- /dev/null +++ b/pycord/guild_scheduled_event.py @@ -0,0 +1,125 @@ +# cython: language_level=3 +# Copyright (c) 2022-present Pycord Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE + +from datetime import datetime + +from pycord.user import User + +from .enums import GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel, GuildScheduledEventStatus +from .missing import Maybe, MISSING +from .mixins import Identifiable +from .asset import Asset + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from discord_typings import GuildScheduledEventData, GuildScheduledEventEntityMetadataData + + from .state import State + +__all__ = ( + "GuildScheduledEvent", + "GuildScheduledEventEntityMetadata", +) + + +class GuildScheduledEvent(Identifiable): + __slots__ = ( + "_state", + "id", + "guild_id", + "channel_id", + "creator_id", + "name", + "description", + "scheduled_start_time", + "scheduled_end_time", + "privacy_level", + "status", + "entity_type", + "entity_id", + "entity_metadata", + "creator", + "user_count", + "image_hash", + ) + + def __init__(self, data: "GuildScheduledEventData", state: "State") -> None: + self._state: "State" = state + self._update(data) + + def _update(self, data: "GuildScheduledEventData") -> None: + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + self.channel_id: int | None = int(cid) if (cid := data.get("channel_id")) else None + self.creator_id: int | None = int(crid) if (crid := data.get("creator_id")) else None + self.name: str = data["name"] + self.description: Maybe[str | None] = data.get("description", MISSING) + self.scheduled_start_time: datetime = datetime.fromisoformat(data["scheduled_start_time"]) + self.scheduled_end_time: datetime | None = datetime.fromisoformat(end) if ( + end := data.get("scheduled_end_time")) else None + self.privacy_level: GuildScheduledEventPrivacyLevel = GuildScheduledEventPrivacyLevel(data["privacy_level"]) + self.status: GuildScheduledEventStatus = GuildScheduledEventStatus(data["status"]) + self.entity_type: GuildScheduledEventEntityType = GuildScheduledEventEntityType(data["entity_type"]) + self.entity_id: int | None = int(eid) if (eid := data.get("entity_id")) else None + self.entity_metadata: GuildScheduledEventEntityMetadata | None = GuildScheduledEventEntityMetadata.from_data( + mdata + ) if (mdata := data.get("entity_metadata")) else None + self.creator: Maybe[User] = User(data=data["creator"], state=self._state) if (data.get("creator")) else MISSING + self.user_count: Maybe[int] = data.get("user_count", MISSING) + self.image_hash: Maybe[str] = data.get("image", MISSING) + + @property + def cover_image(self) -> Asset | None: + return Asset.from_guild_scheduled_event_cover( + self._state, self.guild_id, self.image_hash + ) if self.image_hash else None + + async def modify(self, **kwargs) -> "GuildScheduledEvent": + # TODO: Implement + raise NotImplementedError + + async def delete(self) -> None: + # TODO: Implement + raise NotImplementedError + + async def get_users(self) -> list[User]: + # TODO: Implement + raise NotImplementedError + + +class GuildScheduledEventEntityMetadata: + __slots__ = ( + "location", + ) + + def __init__(self, *, location: Maybe[str] = MISSING) -> None: + self.location: Maybe[str] = location + + @classmethod + def from_data(cls, data: "GuildScheduledEventEntityMetadataData") -> "GuildScheduledEventEntityMetadata": + return cls(**data) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.__repr__() diff --git a/pycord/guild_template.py b/pycord/guild_template.py index e9d3d612..03010f25 100644 --- a/pycord/guild_template.py +++ b/pycord/guild_template.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,44 +18,57 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING -from .snowflake import Snowflake -from .types import GuildTemplate as DiscordGuildTemplate +from .guild import Guild from .user import User +from .missing import Maybe, MISSING if TYPE_CHECKING: + from discord_typings import GuildTemplateData + from .state import State +__all__ = ( + "GuildTemplate", +) + class GuildTemplate: - __slots__ = ( - 'code', - 'name', - 'description', - 'usage_count', - 'creator_id', - 'creator', - 'created_at', - 'updated_at', - 'source_guild_id', - 'serialized_source_guild', - 'is_dirty', - ) - - def __init__(self, data: DiscordGuildTemplate, state: State) -> None: - self.code: str = data['code'] - self.name: str = data['name'] - self.description: str | None = data['description'] - self.usage_count: int = data['usage_count'] - self.creator_id: Snowflake = Snowflake(data['creator_id']) - self.creator: User = User(data['creator'], state) - self.created_at: datetime = datetime.fromisoformat(data['created_at']) - self.updated_at: datetime = datetime.fromisoformat(data['updated_at']) - self.source_guild_id: Snowflake = Snowflake(data['source_guild_id']) - # TODO: maybe make this a Guild object? - self.serialized_source_guild: dict = data['serialized_source_guild'] - self.is_dirty: bool | None = data['is_dirty'] + def __init__(self, data: "GuildTemplateData", state: "State") -> None: + self._state: "State" = state + self._update(data) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + def _update(self, data: "GuildTemplateData") -> None: + self._data = data + self.code: str = data["code"] + self.name: str = data["name"] + self.description: str | None = data["description"] + self.usage_count: int = data["usage_count"] + self.creator_id: int = int(data["creator_id"]) + self.creator: User = User(data["creator"], self._state) + self.created_at: datetime = datetime.fromisoformat(data["created_at"]) + self.updated_at: datetime = datetime.fromisoformat(data["updated_at"]) + self.source_guild_id: int = int(data["source_guild_id"]) + self.serialized_source_guild: Guild = Guild(data["serialized_source_guild"], self._state) + self.is_dirty: bool | None = data["is_dirty"] + + async def sync(self) -> "GuildTemplate": + # TODO: implement + raise NotImplementedError + + async def modify(self, *, name: Maybe[str] = MISSING, description: Maybe[str] = MISSING) -> "GuildTemplate": + # TODO: implement + raise NotImplementedError + + async def delete(self) -> None: + # TODO: implement + raise NotImplementedError diff --git a/pycord/http.py b/pycord/http.py deleted file mode 100644 index 6509cc45..00000000 --- a/pycord/http.py +++ /dev/null @@ -1,278 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, TypeVar - -from aiohttp import BasicAuth - -from .api import HTTPClient -from .commands.application import ApplicationCommand -from .errors import PycordException -from .interaction import Interaction -from .missing import MISSING, Maybe -from .state.core import State -from .types import ApplicationCommand as RawCommand, AsyncFunc -from .user import User -from .utils import compare_application_command, remove_undefined - -try: - from nacl.exceptions import BadSignatureError - from nacl.signing import VerifyKey - - has_nacl = True -except ImportError: - has_nacl = False - -if TYPE_CHECKING: - from fastapi import Request - -T = TypeVar('T') - - -class DiscordException(PycordException): - ... - - -class Pycord: - """HTTP interactions & OAuth implementation.""" - - def __init__( - self, - token: str, - public_key: str, - synchronize: bool = True, - proxy: str | None = None, - proxy_auth: BasicAuth | None = None, - ) -> None: - if not has_nacl: - raise RuntimeError('PyNaCl must be installed to use HTTP Interactions.') - - self.__token = token - self._commands: dict[str, ApplicationCommand] = {} - self.synchronize_commands = synchronize - self.public_key = public_key - self.verify_key = VerifyKey(bytes.fromhex(self.public_key)) - self.http = HTTPClient(token, proxy=proxy, proxy_auth=proxy_auth) - self.__state = State() - """Fake state used primarily inside classes. Should not be used otherwise.""" - - self.__state.http = self.http - - def verify_signature(self, sig: str, ts: str, body: str) -> None: - try: - self.verify_key.verify(f'{ts}{body}'.encode(), bytes.fromhex(sig)) - except BadSignatureError: - raise DiscordException('Invalid request signature') - - def command( - self, - name: Maybe[str] = MISSING, - cls: T = ApplicationCommand, - **kwargs: Any, - ) -> T: - """ - Create a command. - - Parameters - ---------- - name: :class:`str` - The name of the Command. - cls: type of :class:`.commands.Command` - The command type to instantiate. - kwargs: dict[str, Any] - The kwargs to entail onto the instantiated command. - """ - - def wrapper(func: AsyncFunc) -> T: - command = cls(func, name=name, state=self.__state, **kwargs) - self._commands[command.name] = command - return command - - return wrapper - - async def fastapi(self, request: Request) -> dict[str, Any]: - # TODO: support modals - self.verify_signature( - request.headers.get('X-Signature-Ed25519', ''), - request.headers.get('X-Signature-Timestamp', ''), - (await request.body()).decode('utf-8'), - ) - - data = await request.json() - type = data['type'] - name = data['data']['name'] - - if type == 1: - return {'type': 1} - - cmd = self._commands[name] - - proc: Interaction = cmd._processor_event._interaction_object( - data, self.__state, response=True, save=True - ) - - await cmd._invoke(proc) - - return proc.response.raw_response or {} - - @property - def user(self) -> User: - """The most recent version of this bot's user. Should not be used outside of commands.""" - - if self.__state.user is None: - raise AttributeError - - return self.__state.user - - async def setup(self) -> None: - self.__state.user = User(await self.http.get_current_user(), self.__state) - - if not self.synchronize_commands: - return - - global_commands: list[ - RawCommand - ] = await self.http.get_global_application_commands(self.user.id, True) - guild_commands: dict[int, list[RawCommand]] = {} - global_command_names = [c['name'] for c in global_commands] - guild_command_names: dict[int, list[str]] = { - gid: [cmd['name'] for cmd in cmds] for gid, cmds in guild_commands.items() - } - - active_commands: dict[str, ApplicationCommand] = { - name: command for name, command in self._commands.items() - } - - # firstly, fetch commands from guilds - for command in self._commands.values(): - if ( - command.guild_id is not None - and command.guild_id not in guild_commands.values() - ): - guild_commands[ - command.guild_id - ] = await self.http.get_guild_application_commands( - self.user.id, command.guild_id, True - ) - - for command in self._commands.values(): - if not command.guild_id and command.name not in global_command_names: - cmd = await self.http.create_global_application_command( - self.user.id, - name=command.name, - name_localizations=command.name_localizations, - description=command.description, - description_localizations=command.description_localizations, - options=[option.to_dict() for option in command.options], - default_member_permissions=command.default_member_permissions, - dm_permission=command.dm_permission, - type=command.type, - ) - command.id = int(cmd['id']) - elif command.guild_id and command.name not in guild_command_names.get( - command.guild_id, [] - ): - cmd = await self.http.create_guild_application_command( - self.user.id, - command.guild_id, - name=command.name, - name_localizations=command.name_localizations, - description=command.description, - description_localizations=command.description_localizations, - options=[option.to_dict() for option in command.options], - default_member_permissions=command.default_member_permissions, - dm_permission=command.dm_permission, - type=command.type, - ) - command.id = int(cmd['id']) - elif command.guild_id: - for cmd in guild_commands[command.guild_id]: - if cmd['name'] == command.name: - command.id = int(cmd['id']) - break - else: - raise RuntimeError() - - options = [option.to_dict() for option in command.options] - - if options == []: - options = MISSING - - if not compare_application_command(command, cmd): - await self.http.edit_guild_application_command( - self.user.id, - command.id, - command.guild_id, - **remove_undefined( - default_member_permissions=command.default_member_permissions, - name_localizations=command.name_localizations, - description=command.description, - description_localizations=cmd.description_localizations, - dm_permission=command.dm_permission, - nsfw=command.nsfw, - options=options, - ), - ) - elif not command.guild_id: - for cmd in global_commands: - if cmd['name'] == command.name: - command.id = int(cmd['id']) - break - else: - raise RuntimeError() - - options = [option.to_dict() for option in command.options] - - if options == []: - options = MISSING - - if not compare_application_command(command, cmd): - await self.http.edit_global_application_command( - self.user.id, - command.id, - **remove_undefined( - default_member_permissions=command.default_member_permissions, - name_localizations=command.name_localizations, - description=command.description, - description_localizations=cmd.description_localizations, - dm_permission=command.dm_permission, - nsfw=command.nsfw, - options=options, - ), - ) - - # now we can synchronize - for command in global_commands: - if command['name'] not in active_commands: - await self.http.delete_global_application_command( - self.user.id, command['id'] - ) - continue - - for commands in guild_commands.values(): - for command in commands: - if command['name'] not in active_commands: - await self.http.delete_guild_application_command( - self.user.id, command['guild_id'], command['id'] - ) - continue diff --git a/pycord/integration.py b/pycord/integration.py deleted file mode 100644 index 26db89f6..00000000 --- a/pycord/integration.py +++ /dev/null @@ -1,96 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING - -from .application import Application -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import ( - INTEGRATION_EXPIRE_BEHAVIOR, - INTEGRATION_TYPE, - SCOPE, - Account as DiscordAccount, - Integration as DiscordIntegration, - IntegrationApplication as DiscordIntegrationApplication, -) -from .user import User - -if TYPE_CHECKING: - from .state import State - - -class Account: - def __init__(self, data: DiscordAccount) -> None: - self.id: str = data['id'] - self.name: str = data['name'] - - -class IntegrationApplication: - def __init__(self, data: DiscordIntegrationApplication, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.icon: str | None = data['icon'] - self.description: str | None = data['description'] - self.bot: User | MissingEnum = ( - User(data['bot'], state) if data.get('bot') is not None else MISSING - ) - - -class Integration: - def __init__(self, data: DiscordIntegration, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.type: INTEGRATION_TYPE = data['type'] - self.enabled: bool | MissingEnum = data.get('enabled', MISSING) - self.syncing: bool | MissingEnum = data.get('syncing', MISSING) - self.role_id: Snowflake | MissingEnum = ( - Snowflake(data['role_id']) if data.get('role_id') else MISSING - ) - self.enable_emoticons: bool | MissingEnum = data.get( - 'enable_emoticons', MISSING - ) - self.expire_behavior: INTEGRATION_EXPIRE_BEHAVIOR | MissingEnum = data.get( - 'expire_behavior', MISSING - ) - self.expire_grace_period: int | MissingEnum = data.get( - 'expire_grace_period', MISSING - ) - self.user: User | MissingEnum = ( - User(data['user'], state) if data.get('user') is not None else MISSING - ) - self.account: Account = Account(data['account']) - self.synced_at: MissingEnum | datetime = ( - datetime.fromisoformat(data['synced_at']) - if data.get('synced_at') is not None - else MISSING - ) - self.subscriber_count: int | MissingEnum = data.get('subscriber_count', MISSING) - self.revoked: bool | MissingEnum = data.get('revoked', MISSING) - self.application: Application | MissingEnum = ( - IntegrationApplication(data['application'], state) - if data.get('application') is not None - else MISSING - ) - self.scopes: list[SCOPE] | MissingEnum = data.get('scopes', MISSING) diff --git a/pycord/interaction.py b/pycord/interaction.py deleted file mode 100644 index 1b6184a0..00000000 --- a/pycord/interaction.py +++ /dev/null @@ -1,253 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from functools import cached_property -from typing import TYPE_CHECKING - -from .embed import Embed -from .errors import InteractionException -from .flags import MessageFlags -from .member import Member -from .message import Message -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import INTERACTION_DATA, Interaction as InteractionData -from .user import User -from .webhook import Webhook - -if TYPE_CHECKING: - from .state import State - from .ui.text_input import Modal - - -class InteractionOption: - __slots__ = ('name', 'type', 'value', 'options', 'focused') - - def __init__( - self, - name: str, - type: int, - value: str | int | float | MissingEnum = MISSING, - options: list[InteractionOption] = [], - focused: bool | MissingEnum = MISSING, - ) -> None: - self.name = name - self.type = type - self.value = value - self.options = options - self.focused = focused - - -class Interaction: - __slots__ = ( - '_state', - 'response', - 'id', - 'application_id', - 'type', - 'data', - 'guild_id', - 'channel_id', - 'member', - 'user', - 'token', - 'version', - 'message', - 'app_permissions', - 'locale', - 'guild_locale', - 'options', - 'command_id', - 'name', - 'application_command_type', - 'resolved', - 'options', - 'guild_id', - 'custom_id', - 'component_type', - 'values', - ) - - def __init__( - self, - data: InteractionData, - state: State, - response: bool = False, - save: bool = False, - ) -> None: - self._state = state - if response: - self.response = InteractionResponse(self, save=save) - self.id = Snowflake(data['id']) - self.application_id = Snowflake(data['application_id']) - self.type = data['type'] - self.data: INTERACTION_DATA | MissingEnum = data.get('data', MISSING) - _guild_id = data.get('guild_id') - self.guild_id: Snowflake | MissingEnum = ( - Snowflake(_guild_id) if _guild_id is not None else MISSING - ) - _channel_id = data.get('channel_id') - self.channel_id: Snowflake | MissingEnum = ( - Snowflake(_channel_id) if _channel_id is not None else MISSING - ) - _member = data.get('member') - self.member = ( - Member(_member, state, guild_id=self.guild_id) - if _member is not None - else MISSING - ) - _user = data.get('user') - if self.member is not MISSING: - self.user = self.member.user - else: - self.user = User(_user, state) if _user is not None else MISSING - self.token = data['token'] - self.version = data['version'] - _message = data.get('message') - self.message: Message | MissingEnum = ( - Message(_message, state) if _message is not None else MISSING - ) - self.app_permissions: str | MissingEnum = data.get('app_permissions', MISSING) - self.locale: str | MissingEnum = data.get('locale', MISSING) - self.guild_locale: str | MissingEnum = data.get('guild_locale', MISSING) - self.options = [] - - # app command data - if self.type == 2: - self.command_id = Snowflake(self.data['id']) - self.name = self.data['name'] - self.application_command_type = self.data['type'] - self.resolved = self.data.get('resolved') - self.options = [ - InteractionOption(**option) for option in self.data.get('options', []) - ] - self.guild_id = ( - Snowflake(data.get('guild_id')) - if data.get('guild_id') is not None - else MISSING - ) - elif self.type == 3: - self.custom_id = self.data['custom_id'] - self.component_type = self.data['component_type'] - self.values = self.data.get('values', MISSING) - - @property - def resp(self) -> InteractionResponse: - return self.response - - -class InteractionResponse: - __slots__ = ('_parent', '_deferred', '_save', 'responded', 'raw_response') - - def __init__(self, parent: Interaction, save: bool) -> None: - self._parent = parent - self.responded: bool = False - self._deferred: bool = False - self._save = save - self.raw_response = None - - @cached_property - def followup(self) -> Webhook: - return Webhook(self._parent.id, self._parent.token) - - async def send( - self, - content: str, - tts: bool = False, - embeds: list[Embed] = [], - flags: int | MessageFlags = 0, - ) -> None: - if self.responded: - raise InteractionException('This interaction has already been responded to') - - if isinstance(flags, MessageFlags): - flags = flags.as_bit - - if self._save: - self.raw_response = { - 'type': 4, - 'data': { - 'content': content, - 'tts': tts, - 'embeds': embeds, - 'flags': flags, - }, - } - self.responded = True - return - - await self._parent._state.http.create_interaction_response( - self._parent.id, - self._parent.token, - { - 'type': 4, - 'data': { - 'content': content, - 'tts': tts, - 'embeds': embeds, - 'flags': flags, - }, - }, - ) - self.responded = True - - async def defer(self) -> None: - if self._deferred or self.responded: - raise InteractionException( - 'This interaction has already been deferred or responded to' - ) - - await self._parent._state.http.create_interaction_response( - self._parent.id, self._parent.token, {'type': 5} - ) - - self._deferred = True - self.responded = True - - async def send_modal(self, modal: Modal) -> None: - if self.responded: - raise InteractionException('This interaction has already been responded to') - - await self._parent._state.http.create_interaction_response( - self._parent.id, self._parent.token, {'type': 9, 'data': modal._to_dict()} - ) - self._parent._state.sent_modal(modal) - self.responded = True - - async def autocomplete(self, choices: list[str]) -> None: - if self.responded: - raise InteractionException('This interaction has already been responded to') - - if self._save: - self.raw_response = { - 'type': 8, - 'data': {'choices': choices}, - } - - await self._state.http.create_interaction_response( - self._parent.id, - self._parent.token, - { - 'type': 8, - 'data': {'choices': choices}, - }, - ) diff --git a/pycord/interface.py b/pycord/interface.py index 33d3d76d..c0a945cf 100644 --- a/pycord/interface.py +++ b/pycord/interface.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,7 +18,7 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. import datetime import importlib.resources import logging @@ -28,30 +29,31 @@ import sys import time import warnings -from typing import Sequence +from typing import Any, Sequence import colorlog from pycord._about import __copyright__, __git_sha1__, __license__, __version__ -__all__: Sequence[str] = ('start_logging', 'print_banner') - +__all__: Sequence[str] = ("start_logging", "print_banner") day_prefixes: dict[int, str] = { - 1: 'st', - 2: 'nd', - 3: 'rd', - 4: 'th', - 5: 'th', - 6: 'th', - 7: 'th', - 8: 'th', - 9: 'th', - 0: 'th', + 1: "st", + 2: "nd", + 3: "rd", + 4: "th", + 5: "th", + 6: "th", + 7: "th", + 8: "th", + 9: "th", + 0: "th", } -def start_logging(flavor: None | int | str | dict, debug: bool = False): +def start_logging( + flavor: None | int | str | dict[str, Any], debug: bool = False +) -> None: if len(logging.root.handlers) != 0: return # the user is most likely using logging.basicConfig, or is being spearheaded by something else. @@ -61,29 +63,29 @@ def start_logging(flavor: None | int | str | dict, debug: bool = False): if isinstance(flavor, dict): logging.config.dictConfig(flavor) - if flavor.get('handler'): + if flavor.get("handler"): return flavor = None # things that will never be logged. - logging.logThreads = None - logging.logProcesses = None + logging.logThreads = False + logging.logProcesses = False colorlog.basicConfig( level=flavor, - format='%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: ' - '%(thin)s%(message)s%(reset)s', + format="%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: " + "%(thin)s%(message)s%(reset)s", stream=sys.stderr, log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red, bg_white', + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red, bg_white", }, ) - warnings.simplefilter('always', DeprecationWarning) + warnings.simplefilter("always", DeprecationWarning) logging.captureWarnings(True) @@ -95,33 +97,34 @@ def get_day_prefix(num: int) -> str: def print_banner( concurrency: int, shard_count: int, - bot_name: str = 'Your bot', - module: str | None = 'pycord', -): + bot_name: str = "Your bot", + module: str = "pycord.panes", +) -> None: banners = importlib.resources.files(module) for trav in banners.iterdir(): - if trav.name == 'banner.txt': + if trav.name == "banner.txt": banner = trav.read_text() - elif trav.name == 'ibanner.txt': + elif trav.name == "informer.txt": + # TODO: redo and *beautify* informer info_banner = trav.read_text() today = datetime.date.today() args = { - 'copyright': __copyright__, - 'version': __version__, - 'license': __license__, + "copyright": __copyright__, + "version": __version__, + "license": __license__, # the # prefix only works on Windows, and the - prefix only works on linux/unix systems - 'current_time': today.strftime(f'%B the %#d{get_day_prefix(today.day)} of %Y') - if os.name == 'nt' - else today.strftime(f'%B the %-d{get_day_prefix(today.day)} of %Y'), - 'py_version': platform.python_version(), - 'git_sha': __git_sha1__[:8], - 'botname': bot_name, - 'concurrency': concurrency, - 'shardcount': shard_count, - 'sp': '' if shard_count == 1 else 's', + "current_time": today.strftime(f"%B the %#d{get_day_prefix(today.day)}, %Y") + if os.name == "nt" + else today.strftime(f"%B the %-d{get_day_prefix(today.day)} of %Y"), + "py_version": platform.python_version(), + "git_sha": __git_sha1__[:8], + "botname": bot_name, + "concurrency": concurrency, + "shardcount": shard_count, + "sp": "" if shard_count == 1 else "s", } args |= colorlog.escape_codes.escape_codes diff --git a/pycord/internal/__init__.py b/pycord/internal/__init__.py new file mode 100644 index 00000000..4b0cfe74 --- /dev/null +++ b/pycord/internal/__init__.py @@ -0,0 +1,12 @@ +""" +pycord.internal +~~~~~~~~~~~~~~~ +Absolute internal components for interacting with the Discord API. + +:copyright: 2021-present Pycord +:license: MIT, see LICENSE for more info. +""" + +from .event_manager import * +from .gateway import * +from .http import * diff --git a/pycord/internal/event_manager.py b/pycord/internal/event_manager.py new file mode 100644 index 00000000..98b1e7d0 --- /dev/null +++ b/pycord/internal/event_manager.py @@ -0,0 +1,94 @@ +# MIT License +# +# Copyright (c) 2023 Pycord +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import asyncio +from typing import Any, Callable, Coroutine, Self, Type + +from mypy_extensions import i32, mypyc_attr, trait + +from ..task_descheduler import tasks + + +class Filters: + def __init__(self, cls: Type["EventTrait"], filters: dict[str, Any] | None) -> None: + self.event = cls + self.filters = filters or {} + + +@trait +@mypyc_attr(allow_interpreted_subclasses=True) +class EventTrait: + _name: str = "UNDEFINED" + + async def on(self, data: dict[str, Any], filters: Filters) -> Self: + return self + + @classmethod + def filter(cls, **filters: Any) -> Filters: + return Filters(cls, filters) + + +EventFunc = Callable[[EventTrait], Coroutine[Any, Any, None]] + + +class EventManager: + def __init__(self) -> None: + self._events: dict[Type[EventTrait], list[tuple[Filters, EventFunc]]] = {} + + async def push(self, name: str, data: dict[str, Any]) -> i32: + ret = 0 + async with tasks() as tg: + for event, funcs in self._events.copy().items(): + e = event() + if event._name == name: + for filt, func in funcs: + ret += 1 + res = await e.on(data, filt) + tg[asyncio.create_task(func(res))] + + return ret + + def add_event(self, event: Type[EventTrait] | Filters, func: EventFunc) -> None: + if isinstance(event, Filters): + event_class = event.event + filters = event.filters + else: + event_class = event + filters = {} + + if event_class in self._events: + self._events[event_class].append((Filters(event_class, filters), func)) + else: + self._events[event_class] = [(Filters(event_class, filters), func)] + + def remove_event(self, event: Type[EventTrait] | Filters, func: EventFunc) -> None: + if isinstance(event, Filters): + event_class = event.event + else: + event_class = event + + if event_class in self._events: + for f, func in self._events[event_class]: + if func == func: + self._events[event_class].remove((f, func)) + break diff --git a/pycord/internal/gateway.py b/pycord/internal/gateway.py new file mode 100644 index 00000000..0239ecc1 --- /dev/null +++ b/pycord/internal/gateway.py @@ -0,0 +1,128 @@ +# MIT License +# +# Copyright (c) 2023 Pycord +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING, Protocol + +from aiohttp import BasicAuth +from discord_typings import GetGatewayBotData +from mypy_extensions import trait + +from ..task_descheduler import tasks +from .reserver import Reserver +from .shard import Shard + +if TYPE_CHECKING: + from ..state.core import State + + +@trait +class GatewayProtocol: + def __init__(self, state: State) -> None: + ... + + async def start(self) -> None: + ... + + async def get_guild_members( + self, + guild_id: int, + query: str = "", + limit: int = 0, + presences: bool = False, + user_ids: list[int] = [], + nonce: str | None = None, + ) -> None: + """Get members inside of a Guild. + + Parameters + ---------- + guild_id: :class:`int` + The guild id of the members. + query: :class:`str` + The query to use. Defaults to an empty string, or all members. + limit: :class:`int` + Only return `limit` amount of members. + presences: :class:`bool` + Whether to return presences with the members. + user_ids: list[:class:`int`] + A list of users to return the member objects of. + nonce: :class:`str` + A custom nonce to replace Pycord's own unique nonce. + """ + + +class Gateway(GatewayProtocol): + def __init__( + self, + state: State, + version: int, + proxy: str | None, + proxy_auth: BasicAuth | None, + # shards + shard_ids: list[int], + shard_total: int, + ) -> None: + self._state: "State" = state + self.shards: list[Shard] = [] + self.shard_ids = shard_ids + self.shard_total = shard_total + self.version = version + self.proxy = proxy + self.proxy_auth = proxy_auth + + async def start(self, gateway_data: GetGatewayBotData) -> None: + self._state.shard_rate_limit = Reserver( + gateway_data["session_start_limit"]["max_concurrency"], 5 + ) + self._state.shard_rate_limit.current = gateway_data["session_start_limit"][ + "remaining" + ] + + for shard_id in self.shard_ids: + self.shards.append( + Shard( + self._state, + shard_id, + self.shard_total, + version=self.version, + proxy=self.proxy, + proxy_auth=self.proxy_auth, + ) + ) + + for shard in self.shards: + async with tasks() as tg: + tg[asyncio.create_task(shard.connect())] + + async def get_guild_members( + self, + guild_id: int, + query: str = "", + limit: int = 0, + presences: bool = False, + user_ids: list[int] = [], + nonce: str | None = None, + ) -> None: + pass diff --git a/pycord/internal/http.py b/pycord/internal/http.py new file mode 100644 index 00000000..650a9893 --- /dev/null +++ b/pycord/internal/http.py @@ -0,0 +1,3848 @@ +# MIT License +# +# Copyright (c) 2023 Pycord +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +import sys +from datetime import datetime +from typing import Any, cast, Literal, Type, TYPE_CHECKING, TypeVar + +import aiohttp +from aiohttp import __version__ as aiohttp_version, BasicAuth, ClientSession, FormData +from discord_typings import ( + AllowedMentionsData, ApplicationCommandData, ApplicationCommandOptionData, ApplicationCommandPermissionsData, + ApplicationCommandTypes, ApplicationData, ApplicationRoleConnectionData, ApplicationRoleConnectionMetadataData, + AuditLogData, AuditLogEvents, AutoModerationActionData, AutoModerationEventTypes, AutoModerationRuleData, + AutoModerationTriggerMetadataData, AutoModerationTriggerTypes, BanData, ChannelData, ChannelTypes, ComponentData, + ConnectionData, DefaultMessageNotificationLevels, DefaultReactionData, EmbedData, EmojiData, + ExplicitContentFilterLevels, FollowedChannelData, ForumLayoutTypes, ForumTagData, GetGatewayBotData, + GuildApplicationCommandPermissionData, GuildData, GuildFeatures, GuildMemberData, GuildOnboardingData, + GuildOnboardingModes, GuildOnboardingPromptsData, GuildPreviewData, GuildScheduledEventData, + GuildScheduledEventEntityMetadataData, GuildScheduledEventEntityTypes, GuildScheduledEventPrivacyLevels, + GuildScheduledEventStatus, GuildScheduledEventUserData, GuildTemplateData, GuildWidgetData, GuildWidgetSettingsData, + HasMoreListThreadsData, InstallParams, IntegrationData, InviteData, MessageData, MessageReferenceData, MFALevels, + PartialAttachmentData, PartialGuildData, PermissionOverwriteData, RoleData, SortOrderTypes, StageInstanceData, + StageInstancePrivacyLevels, StickerData, StickerPackData, ThreadMemberData, UserData, VerificationLevels, + VoiceRegionData, WebhookData, WelcomeChannelData, WelcomeScreenData, +) +from msgspec import json + +from .._about import __version__ +from ..errors import BotException, DiscordException, Forbidden, HTTPException, NotFound +from ..file import File +from ..missing import Maybe, MISSING, MissingEnum +from ..types.channel import ( + ChannelPositionUpdateData, ForumThreadMessageParams, +) +from ..types.guild import ( + MFALevelResponse, + PruneCountResponse, + RolePositionUpdateData, VanityURLData, +) +from ..utils import form_qs, remove_undefined, to_datauri + +_log = logging.getLogger(__name__) + +T = TypeVar("T") + + +class Route: + def __init__( + self, + client: "HTTPClient", + path: str, + guild_id: int | None = None, + channel_id: int | None = None, + webhook_id: int | None = None, + webhook_token: str | None = None, + **parameters: object, + ) -> None: + self.client = client # TODO: why? + self.path: str = path.format(**parameters) + self.guild_id = guild_id + self.channel_id = channel_id + self.webhook_id = webhook_id + self.webhook_token = webhook_token + self.parameters = parameters + + @property + def bucket(self) -> str: + return f"{self.path}:{self.guild_id}:{self.channel_id}:{self.webhook_id}:{self.webhook_token}" + + +class HTTPClient: + def __init__( + self, + token: str | None = None, + base_url: str = "https://discord.com/api/v10", + proxy: str | None = None, + proxy_auth: BasicAuth | None = None, + ) -> None: + self.base_url = base_url + self._proxy = proxy + self._proxy_auth = proxy_auth + self._headers = { + "User-Agent": "DiscordBot (https://pycord.dev, {0}) Python/{1[0]}.{1[1]} aiohttp/{2}".format( + __version__, sys.version_info, aiohttp_version + ), + } + if token: + self._headers["Authorization"] = f"Bot {token}" + + self._session: None | ClientSession = None + + async def force_start(self) -> None: + if self._session is None: + self._session = aiohttp.ClientSession() + + async def request( # type: ignore[return] + self, + method: str, + route: Route, + data: dict[str, Any] | list[Any] | None = None, + files: list[File] | None = None, + add_files: bool = True, + form: list[dict[str, Any]] | None = None, + *, + reason: str | None = None, + query_params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + t: Type[T], + ) -> T: + endpoint = self.base_url + route.path + + if self._session is None: + self._session = aiohttp.ClientSession() + + if TYPE_CHECKING: + assert self._session + + _headers = self._headers.copy() + if headers: + _headers.update(headers) + + if reason: + headers["X-Audit-Log-Reason"] = reason + + payload: bytes | FormData + + if data: + data = json.encode(data) + headers.update({"Content-Type": "application/json"}) + + if form and data: + form.append({"name": "payload_json", "value": data}) + + if files and add_files: + if not form: + form = [] + + for idx, file in enumerate(files): + form.append( + { + "name": f"files[{idx}]", + "value": file.file.read(), + "filename": file.filename, + "content_type": "application/octet-stream", + } + ) + + _log.debug(f"Requesting to {endpoint} with {data}, {headers}") + + for try_ in range(5): + if files: + for file in files: + file.reset(try_) + + if form: + payload = FormData(quote_fields=False) + for params in form: + payload.add_field(**params) + + r = await self._session.request( + method, + endpoint, + data=data, + headers=_headers, + proxy=self._proxy, + proxy_auth=self._proxy_auth, + params=query_params, + ) + _log.debug(f"Received back {await r.text()}") + + if t: + d = cast(T, json.decode(await r.read())) + else: + d = cast(T, await r.text()) + + if r.status == 429: + # TODO: properly handle rate limits + _log.debug(f"Request to {endpoint} failed: Request returned rate limit") + raise HTTPException(r, d, data) + + elif r.status == 403: + raise Forbidden(r, d, data) + elif r.status == 404: + raise NotFound(r, d, data) + elif r.status == 500: + raise DiscordException(r, d, data) + elif r.ok: + return d + else: + raise HTTPException(r, d, data) + + # cdn + async def get_from_cdn(self, url: str) -> bytes: + if self._session is None: + self._session = aiohttp.ClientSession() + + if TYPE_CHECKING: + assert self._session + + r = await self._session.request( + "GET", + url, + ) + d = await r.read() + if r.status == 403: + raise Forbidden(r, d, None) + elif r.status == 404: + raise NotFound(r, d, None) + elif r.status == 500: + raise DiscordException(r, d, None) + elif r.ok: + return d + else: + raise HTTPException(r, d, None) + + # Application + async def get_current_application(self) -> ApplicationData: + return cast( + ApplicationData, + await self.request( + 'GET', Route(self, '/oauth2/applications/@me'), t=ApplicationData, + ) + ) + + async def edit_current_application( + self, + custom_install_url: Maybe[str] = MISSING, + description: Maybe[str] = MISSING, + role_connections_verification_url: Maybe[str] = MISSING, + install_params: Maybe[InstallParams] = MISSING, + flags: Maybe[int] = MISSING, + icon: Maybe[File | None] = MISSING, + cover_image: Maybe[File | None] = MISSING, + interactions_endpoint_url: Maybe[str] = MISSING, + tags: Maybe[list[str]] = MISSING, + ) -> ApplicationData: + data = remove_undefined( + custom_install_url=custom_install_url, + description=description, + role_connections_verification_url=role_connections_verification_url, + install_params=install_params, + flags=flags, + icon=to_datauri(icon) if icon else icon, + cover_image=to_datauri(cover_image) if cover_image else cover_image, + interactions_endpoint_url=interactions_endpoint_url, + tags=tags, + ) + + return cast( + ApplicationData, + await self.request( + 'PATCH', Route(self, '/oauth2/applications/@me'), data, t=ApplicationData, + ) + ) + + async def get_gateway_bot(self) -> GetGatewayBotData: + return cast( + GetGatewayBotData, + await self.request( + 'GET', Route(self, '/gateway/bot'), t=GetGatewayBotData, + ) + ) + + # Application Commands + async def get_global_application_commands( + self, application_id: int, with_localizations: bool = False + ) -> list[ApplicationCommandData]: + return cast( + list[ApplicationCommandData], + await self.request( + 'GET', + Route( + self, + '/applications/{application_id}/commands', application_id=application_id + ), + query_params={'with_localizations': str(with_localizations).lower()}, + t=list[ApplicationCommandData], + ) + ) + + async def create_global_application_command( + self, + application_id: int, + name: str, + name_localizations: Maybe[dict[str, str]] = MISSING, + description: Maybe[str] = MISSING, + description_localizations: Maybe[dict[str, str]] = MISSING, + options: Maybe[list[ApplicationCommandOptionData]] = MISSING, + default_member_permissions: Maybe[str | None] = MISSING, + dm_permission: Maybe[bool | None] = MISSING, + default_permission: Maybe[bool] = MISSING, + type: Maybe[ApplicationCommandTypes] = MISSING, + ) -> ApplicationCommandData: + data = remove_undefined( + name=name, + name_localizations=name_localizations, + description=description, + description_localizations=description_localizations, + options=options, + default_member_permissions=default_member_permissions, + dm_permission=dm_permission, + default_permission=default_permission, + type=type, + ) + + return cast( + ApplicationCommandData, + await self.request( + 'POST', + Route( + self, + '/applications/{application_id}/commands', application_id=application_id + ), + data=data, + t=ApplicationCommandData, + ) + ) + + async def edit_global_application_command( + self, + application_id: int, + command_id: int, + name: Maybe[str] = MISSING, + name_localizations: Maybe[dict[str, str]] = MISSING, + description: Maybe[str] = MISSING, + description_localizations: Maybe[dict[str, str]] = MISSING, + options: Maybe[list[ApplicationCommandOptionData]] = MISSING, + default_member_permissions: Maybe[str | None] = MISSING, + dm_permission: Maybe[bool | None] = MISSING, + default_permission: Maybe[bool] = MISSING, + type: Maybe[ApplicationCommandTypes] = MISSING, + ) -> ApplicationCommandData: + data = remove_undefined( + name=name, + name_localizations=name_localizations, + description=description, + description_localizations=description_localizations, + options=options, + default_member_permissions=default_member_permissions, + dm_permission=dm_permission, + default_permission=default_permission, + type=type, + ) + + return cast( + ApplicationCommandData, + await self.request( + 'PATCH', + Route( + self, + '/applications/{application_id}/commands/{command_id}', + application_id=application_id, + command_id=command_id, + ), + data=data, + t=ApplicationCommandData, + ) + ) + + async def delete_global_application_command( + self, + application_id: int, + command_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/applications/{application_id}/commands/{command_id}', + application_id=application_id, + command_id=command_id, + ), + t=None, + ) + ) + + async def get_guild_application_commands( + self, + application_id: int, + guild_id: int, + with_localizations: bool = False, + ) -> list[ApplicationCommandData]: + return cast( + list[ApplicationCommandData], + await self.request( + 'GET', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands', + application_id=application_id, + guild_id=guild_id, + ), + query_params={'with_localizations': str(with_localizations).lower()}, + t=list[ApplicationCommandData], + ) + ) + + async def create_guild_application_command( + self, + application_id: int, + guild_id: int, + name: str, + name_localizations: Maybe[dict[str, str]] = MISSING, + description: Maybe[str] = MISSING, + description_localizations: Maybe[dict[str, str]] = MISSING, + options: Maybe[list[ApplicationCommandOptionData]] = MISSING, + default_member_permissions: Maybe[str | None] = MISSING, + dm_permission: Maybe[bool | None] = MISSING, + default_permission: Maybe[bool] = MISSING, + type: Maybe[ApplicationCommandTypes] = MISSING, + ) -> ApplicationCommandData: + data = remove_undefined( + name=name, + name_localizations=name_localizations, + description=description, + description_localizations=description_localizations, + options=options, + default_member_permissions=default_member_permissions, + dm_permission=dm_permission, + default_permission=default_permission, + type=type, + ) + + return cast( + ApplicationCommandData, + await self.request( + 'POST', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands', + application_id=application_id, + guild_id=guild_id, + ), + data=data, + t=ApplicationCommandData, + ) + ) + + async def edit_guild_application_command( + self, + application_id: int, + command_id: int, + guild_id: int, + name: Maybe[str] = MISSING, + name_localizations: Maybe[dict[str, str]] = MISSING, + description: Maybe[str] = MISSING, + description_localizations: Maybe[dict[str, str]] = MISSING, + options: Maybe[list[ApplicationCommandOptionData]] = MISSING, + default_member_permissions: Maybe[str | None] = MISSING, + dm_permission: Maybe[bool | None] = MISSING, + default_permission: Maybe[bool] = MISSING, + type: Maybe[ApplicationCommandTypes] = MISSING, + ) -> ApplicationCommandData: + data = remove_undefined( + name=name, + name_localizations=name_localizations, + description=description, + description_localizations=description_localizations, + options=options, + default_member_permissions=default_member_permissions, + dm_permission=dm_permission, + default_permission=default_permission, + type=type, + ) + + return cast( + ApplicationCommandData, + await self.request( + 'PATCH', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}', + application_id=application_id, + guild_id=guild_id, + command_id=command_id, + ), + data=data, + t=ApplicationCommandData, + ) + ) + + async def delete_guild_application_command( + self, + application_id: int, + guild_id: int, + command_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}', + application_id=application_id, + command_id=command_id, + guild_id=guild_id, + ), + t=None, + ) + ) + + async def bulk_overwrite_global_commands( + self, application_id: int, application_commands: list[ApplicationCommandData] + ) -> list[ApplicationCommandData]: + return cast( + list[ApplicationCommandData], + await self.request( + 'PUT', + Route( + self, + '/applications/{application_id}/commands', application_id=application_id + ), + application_commands, + t=list[ApplicationCommandData], + ) + ) + + async def bulk_overwrite_guild_commands( + self, + application_id: int, + guild_id: int, + application_commands: list[ApplicationCommandData], + ) -> list[ApplicationCommandData]: + return cast( + list[ApplicationCommandData], + await self.request( + 'PUT', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands', + application_id=application_id, + guild_id=guild_id, + ), + application_commands, + t=list[ApplicationCommandData], + ) + ) + + async def get_guild_application_command_permissions( + self, application_id: int, guild_id: int + ) -> list[GuildApplicationCommandPermissionData]: + return cast( + list[GuildApplicationCommandPermissionData], + await self.request( + 'GET', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands/permissions', + guild_id=guild_id, + application_id=application_id, + ), + t=list[GuildApplicationCommandPermissionData], + ) + ) + + async def get_application_command_permissions( + self, application_id: int, guild_id: int, command_id: int + ) -> GuildApplicationCommandPermissionData: + return cast( + GuildApplicationCommandPermissionData, + await self.request( + 'GET', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions', + guild_id=guild_id, + application_id=application_id, + command_id=command_id, + ), + t=GuildApplicationCommandPermissionData, + ) + ) + + async def edit_application_command_permissions( + self, + application_id: int, + guild_id: int, + command_id: int, + permissions: list[ApplicationCommandPermissionsData], + ) -> GuildApplicationCommandPermissionData: + return cast( + GuildApplicationCommandPermissionData, + await self.request( + 'PUT', + Route( + self, + '/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions', + application_id=application_id, + guild_id=guild_id, + command_id=command_id, + ), + {'permissions': permissions}, + t=GuildApplicationCommandPermissionData, + ) + ) + + # Application Role Connections + async def get_application_role_connection_metadata_records( + self, + application_id: int, + ) -> list[ApplicationRoleConnectionMetadataData]: + return cast( + list[ApplicationRoleConnectionMetadataData], + await self.request( + 'GET', + Route( + self, + '/applications/{application_id}/role-connections/metadata', + application_id=application_id, + ), + t=list[ApplicationRoleConnectionMetadataData], + ) + ) + + async def update_application_role_connection_metadata_records( + self, + application_id: int, + records: list[ApplicationRoleConnectionMetadataData], + ) -> list[ApplicationRoleConnectionMetadataData]: + return cast( + list[ApplicationRoleConnectionMetadataData], + await self.request( + 'PUT', + Route( + self, + '/applications/{application_id}/role-connections/metadata', + application_id=application_id, + ), + records, + t=list[ApplicationRoleConnectionMetadataData], + ) + ) + + # Audit Log + async def get_guild_audit_log( + self, + guild_id: int, + user_id: Maybe[int] = MISSING, + action_type: Maybe[AuditLogEvents] = MISSING, + before: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + limit: Maybe[int] = MISSING, + ) -> AuditLogData: + path = form_qs( + "/guilds/{guild_id}/audit-logs", + user_id=user_id, + action_type=action_type, + before=before, + after=after, + limit=limit, + ) + + return cast( + AuditLogData, + await self.request( + "GET", + Route(self, path, guild_id=guild_id), + t=AuditLogData, + ), + ) + + # Auto Moderation + async def list_auto_moderation_rules_for_guild( + self, guild_id: int + ) -> list[AutoModerationRuleData]: + return cast( + list[AutoModerationRuleData], + await self.request( + 'GET', + Route( + self, + '/guilds/{guild_id}/auto-moderation/rules', + guild_id=guild_id, + ), + t=list[AutoModerationRuleData], + ) + ) + + async def get_auto_moderation_rule( + self, + guild_id: int, + rule_id: int, + ) -> AutoModerationRuleData: + return cast( + AutoModerationRuleData, + await self.request( + 'GET', + Route( + self, + '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', + guild_id=guild_id, + rule_id=rule_id, + ), + t=AutoModerationRuleData, + ) + ) + + async def create_auto_moderation_rule( + self, + guild_id: int, + *, + name: str, + event_type: AutoModerationEventTypes, + trigger_type: AutoModerationTriggerTypes, + actions: list[AutoModerationActionData], + trigger_metadata: Maybe[AutoModerationTriggerMetadataData] = MISSING, + enabled: bool = False, + exempt_roles: Maybe[list[int]] = MISSING, + exempt_channels: Maybe[list[int]] = MISSING, + reason: str | None = None, + ) -> AutoModerationRuleData: + data = { + 'name': name, + 'event_type': event_type, + 'trigger_type': trigger_type, + 'trigger_metadata': trigger_metadata, + 'actions': actions, + 'enabled': enabled, + 'exampt_roles': exempt_roles, + 'exempt_channels': exempt_channels, + } + return cast( + AutoModerationRuleData, + await self.request( + 'POST', + Route( + self, + '/guilds/{guild_id}/auto-moderation/rules', + guild_id=guild_id, + ), + remove_undefined(**data), + reason=reason, + t=AutoModerationRuleData, + ) + ) + + async def modify_auto_moderation_rule( + self, + guild_id: int, + rule_id: int, + *, + name: Maybe[str] = MISSING, + event_type: Maybe[AutoModerationEventTypes] = MISSING, + actions: Maybe[list[AutoModerationActionData]] = MISSING, + trigger_metadata: Maybe[AutoModerationTriggerMetadataData | None] = MISSING, + enabled: Maybe[bool] = MISSING, + exempt_roles: Maybe[list[int]] = MISSING, + exempt_channels: Maybe[list[int]] = MISSING, + reason: str | None = None, + ) -> AutoModerationRuleData: + data = { + 'name': name, + 'event_type': event_type, + 'trigger_metadata': trigger_metadata, + 'actions': actions, + 'enabled': enabled, + 'exampt_roles': exempt_roles, + 'exempt_channels': exempt_channels, + } + return cast( + AutoModerationRuleData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', + guild_id=guild_id, + rule_id=rule_id, + ), + remove_undefined(**data), + reason=reason, + t=AutoModerationRuleData, + ) + ) + + async def delete_auto_moderation_rule( + self, guild_id: int, rule_id: int, *, reason: str | None = None + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', + guild_id=guild_id, + rule_id=rule_id, + ), + reason=reason, + t=None, + ) + ) + + # Channel + async def get_channel( + self, + channel_id: int, + ) -> ChannelData: + return cast( + ChannelData, + await self.request( + 'GET', Route(self, '/channels/{channel_id}', channel_id=channel_id), t=ChannelData, + ) + ) + + async def modify_channel( + self, + channel_id: int, + *, + name: Maybe[str] = MISSING, + # Group DM Only + icon: Maybe[File] = MISSING, + # Thread Only + archived: Maybe[bool] = MISSING, + auto_archive_duration: Maybe[int] = MISSING, + locked: Maybe[bool] = MISSING, + invitable: Maybe[bool] = MISSING, + applied_tags: Maybe[list[int]] = MISSING, + # Thread & Guild Channels + rate_limit_per_user: Maybe[int | None] = MISSING, + flags: Maybe[int | None] = MISSING, + # Guild Channels Only + type: Maybe[ChannelTypes] = MISSING, + position: Maybe[int | None] = MISSING, + topic: Maybe[str | None] = MISSING, + nsfw: Maybe[bool | None] = MISSING, + bitrate: Maybe[int | None] = MISSING, + user_imit: Maybe[int | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwriteData] | None] = MISSING, + parent_id: Maybe[int | None] = MISSING, + rtc_region: Maybe[str | None] = MISSING, + video_quality_mode: Maybe[int | None] = MISSING, + default_auto_archive_duration: Maybe[int | None] = MISSING, + available_tags: Maybe[list[ForumTagData] | None] = MISSING, + default_reaction_emoji: Maybe[DefaultReactionData | None] = MISSING, + default_thread_rate_limit_per_user: Maybe[int] = MISSING, + default_sort_order: Maybe[int | None] = MISSING, + default_forum_layout: Maybe[int] = MISSING, + # Reason + reason: str | None = None, + ) -> ChannelData: + data = { + 'name': name, + 'icon': to_datauri(icon) if icon else icon, + 'archived': archived, + 'auto_archive_duration': auto_archive_duration, + 'locked': locked, + 'invitable': invitable, + 'applied_tags': applied_tags, + 'rate_limit_per_user': rate_limit_per_user, + 'flags': flags, + 'type': type, + 'position': position, + 'topic': topic, + 'nsfw': nsfw, + 'bitrate': bitrate, + 'user_limit': user_imit, + 'permission_overwrites': permission_overwrites, + 'parent_id': parent_id, + 'rtc_region': rtc_region, + 'video_quality_mode': video_quality_mode, + 'default_auto_archive_duration': default_auto_archive_duration, + 'available_tags': available_tags, + 'default_reaction_emoji': default_reaction_emoji, + 'default_thread_rate_limit_per_user': default_thread_rate_limit_per_user, + 'default_sort_order': default_sort_order, + 'default_forum_layout': default_forum_layout, + } + return cast( + ChannelData, + await self.request( + 'PATCH', + Route(self, '/channels/{channel_id}', channel_id=channel_id), + remove_undefined(**data), + reason=reason, + t=ChannelData, + ) + ) + + async def delete_channel( + self, + channel_id: int, + *, + reason: str | None = None, + ) -> None: + cast( + None, + await self.request( + 'DELETE', + Route(self, '/channels/{channel_id}', channel_id=channel_id), + reason=reason, + t=None, + ) + ) + + async def get_channel_messages( + self, + channel_id: int, + *, + around: Maybe[int] = MISSING, + before: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + limit: Maybe[int] = MISSING, + ) -> list[MessageData]: + path = form_qs( + '/channels/{channel_id}/messages', + around=around, + before=before, + after=after, + limit=limit, + ) + return cast( + list[MessageData], + await self.request( + 'GET', + Route(self, path, channel_id=channel_id), + t=list[MessageData], + ) + ) + + async def get_channel_message( + self, + channel_id: int, + message_id: int, + ) -> MessageData: + return cast( + MessageData, + await self.request( + 'GET', + Route( + self, + '/channels/{channel_id}/messages/{message_id}', + channel_id=channel_id, + message_id=message_id, + ), + t=MessageData, + ) + ) + + async def create_message( + self, + channel_id: int, + *, + content: Maybe[str] = MISSING, + nonce: Maybe[str] = MISSING, + tts: Maybe[bool] = MISSING, + embeds: Maybe[list[EmbedData]] = MISSING, + allowed_mentions: Maybe[AllowedMentionsData] = MISSING, + message_reference: Maybe[MessageReferenceData] = MISSING, + components: Maybe[list[ComponentData]] = MISSING, + sticker_ids: Maybe[list[int]] = MISSING, + files: Maybe[list[File]] = MISSING, + attachments: Maybe[list[PartialAttachmentData]] = MISSING, + flags: Maybe[int] = MISSING, + ) -> MessageData: + data = remove_undefined( + content=content, + nonce=nonce, + tts=tts, + embeds=embeds, + allowed_mentions=allowed_mentions, + message_reference=message_reference, + components=components, + sticker_ids=sticker_ids, + attachments=attachments, + flags=flags, + ) + return cast( + MessageData, + await self.request( + 'POST', + Route(self, '/channels/{channel_id}/messages', channel_id=channel_id), + data, + files=files, + t=MessageData, + ) + ) + + async def crosspost_message( + self, + channel_id: int, + message_id: int, + ) -> MessageData: + return cast( + MessageData, + await self.request( + 'POST', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/crosspost', + channel_id=channel_id, + message_id=message_id, + ), + t=MessageData, + ) + ) + + async def create_reaction( + self, + channel_id: int, + message_id: int, + emoji: str, + ) -> None: + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', + channel_id=channel_id, + message_id=message_id, + emoji=emoji, + ), + t=None, + ) + ) + + async def delete_own_reaction( + self, + channel_id: int, + message_id: int, + emoji: str, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', + channel_id=channel_id, + message_id=message_id, + emoji=emoji, + ), + t=None, + ) + ) + + async def delete_user_reaction( + self, + channel_id: int, + message_id: int, + emoji: str, + user_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{user_id}', + channel_id=channel_id, + message_id=message_id, + emoji=emoji, + user_id=user_id, + ), + t=None, + ) + ) + + async def get_reactions( + self, + channel_id: int, + message_id: int, + emoji: str, + *, + after: Maybe[int] = MISSING, + limit: Maybe[int] = 25, + ) -> list[UserData]: + path = form_qs( + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', + after=after, + limit=limit, + ) + return cast( + list[UserData], + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + message_id=message_id, + emoji=emoji, + ), + t=list[UserData], + ) + ) + + async def delete_all_reactions( + self, + channel_id: int, + message_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/reactions', + channel_id=channel_id, + message_id=message_id, + ), + t=None, + ) + ) + + async def delete_all_reactions_for_emoji( + self, + channel_id: int, + message_id: int, + emoji: str, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', + channel_id=channel_id, + message_id=message_id, + emoji=emoji, + ), + t=None, + ) + ) + + async def edit_message( + self, + channel_id: int, + message_id: int, + *, + content: Maybe[str] = MISSING, + embeds: Maybe[list[EmbedData]] = MISSING, + flags: Maybe[int] = MISSING, + allowed_mentions: Maybe[AllowedMentionsData] = MISSING, + components: Maybe[list[ComponentData]] = MISSING, + files: Maybe[list[File]] = MISSING, + attachments: Maybe[list[PartialAttachmentData]] = MISSING, + ) -> MessageData: + data = { + 'content': content, + 'embeds': embeds, + 'flags': flags, + 'allowed_mentions': allowed_mentions, + 'components': components, + 'attachments': attachments, + } + return cast( + MessageData, + await self.request( + 'PATCH', + Route( + self, + '/channels/{channel_id}/messages/{message_id}', + channel_id=channel_id, + message_id=message_id, + ), + remove_undefined(**data), + files=files, + t=MessageData, + ) + ) + + async def delete_message( + self, + channel_id: int, + message_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/messages/{message_id}', + channel_id=channel_id, + message_id=message_id, + ), + reason=reason, + t=None, + ) + ) + + async def bulk_delete_messages( + self, + channel_id: int, + *, + messages: list[int], + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'POST', + Route(self, '/channels/{channel_id}/messages/bulk-delete', channel_id=channel_id), + {'messages': messages}, + reason=reason, + t=None, + ) + ) + + async def edit_channel_permissions( + self, + channel_id: int, + overwrite_id: int, + *, + type: int, + allow: Maybe[int | None] = MISSING, + deny: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> None: + data = { + 'allow': str(allow), + 'deny': str(deny), + 'type': type, + } + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/permissions/{overwrite_id}', + channel_id=channel_id, + overwrite_id=overwrite_id, + ), + remove_undefined(**data), + reason=reason, + t=None, + ) + ) + + async def get_channel_invites( + self, + channel_id: int, + ) -> list[InviteData]: + return cast( + list[InviteData], + await self.request( + 'GET', Route(self, '/channels/{channel_id}/invites', channel_id=channel_id), t=list[InviteData], + ) + ) + + async def create_channel_invite( + self, + channel_id: int, + *, + max_age: Maybe[int] = MISSING, + max_uses: Maybe[int] = MISSING, + temporary: Maybe[bool] = MISSING, + unique: Maybe[bool] = MISSING, + target_type: Maybe[int] = MISSING, + target_user_id: Maybe[int] = MISSING, + target_application_id: Maybe[int] = MISSING, + reason: str | None = None, + ) -> InviteData: + data = { + 'max_age': max_age, + 'max_uses': max_uses, + 'temporary': temporary, + 'unique': unique, + 'target_type': target_type, + 'target_user_id': target_user_id, + 'target_application_id': target_application_id, + } + return cast( + InviteData, + await self.request( + 'POST', + Route(self, '/channels/{channel_id}/invites', channel_id=channel_id), + remove_undefined(**data), + reason=reason, + t=InviteData + ) + ) + + async def delete_channel_permission( + self, + channel_id: int, + overwrite_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/permissions/{overwrite_id}', + channel_id=channel_id, + overwrite_id=overwrite_id, + ), + reason=reason, + t=None, + ) + ) + + async def follow_announcement_channel( + self, + channel_id: int, + *, + webhook_channel_id: int, + ) -> FollowedChannelData: + data = { + 'webhook_channel_id': webhook_channel_id, + } + return cast( + FollowedChannelData, + await self.request( + 'POST', + Route( + self, + '/channels/{channel_id}/followers', + channel_id=channel_id, + ), + data, + t=FollowedChannelData, + ) + ) + + async def trigger_typing_indicator( + self, + channel_id: int, + ) -> None: + return cast( + None, + await self.request( + 'POST', Route(self, '/channels/{channel_id}/typing', channel_id=channel_id), t=None + ) + ) + + async def get_pinned_messages( + self, + channel_id: int, + ) -> list[MessageData]: + return cast( + list[MessageData], + await self.request( + 'GET', Route(self, '/channels/{channel_id}/pins', channel_id=channel_id), t=list[MessageData], + ) + ) + + async def pin_message( + self, + channel_id: int, + message_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/pins/{message_id}', + channel_id=channel_id, + message_id=message_id, + ), + reason=reason, + t=None, + ) + ) + + async def unpin_message( + self, + channel_id: int, + message_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/pins/{message_id}', + channel_id=channel_id, + message_id=message_id, + ), + reason=reason, + t=None, + ) + ) + + async def group_dm_add_recipient( + self, + channel_id: int, + user_id: int, + *, + access_token: str, + nick: Maybe[str | None] = MISSING, + ) -> None: + data = { + 'access_token': access_token, + 'nick': nick, + } + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/recipients/{user_id}', + channel_id=channel_id, + user_id=user_id, + ), + data, + t=None, + ) + ) + + async def group_dm_remove_recipient( + self, + channel_id: int, + user_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/recipients/{user_id}', + channel_id=channel_id, + user_id=user_id, + ), + t=None, + ) + ) + + async def start_thread_from_message( + self, + channel_id: int, + message_id: int, + *, + name: str, + auto_archive_duration: Maybe[int] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> ChannelData: + data = { + 'name': name, + 'auto_archive_duration': auto_archive_duration, + 'rate_limit_per_user': rate_limit_per_user, + } + return cast( + ChannelData, + await self.request( + 'POST', + Route( + self, + '/channels/{channel_id}/messages/{message_id}/threads', + channel_id=channel_id, + message_id=message_id, + ), + remove_undefined(**data), + reason=reason, + t=ChannelData, + ) + ) + + async def start_thread_without_message( + self, + channel_id: int, + *, + name: str, + auto_archive_duration: Maybe[int] = MISSING, + type: Maybe[ChannelTypes] = MISSING, + invitable: Maybe[bool] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> ChannelData: + data = { + 'name': name, + 'auto_archive_duration': auto_archive_duration, + 'type': type, + 'invitable': invitable, + 'rate_limit_per_user': rate_limit_per_user, + } + return cast( + ChannelData, + await self.request( + 'POST', + Route( + self, + '/channels/{channel_id}/threads', + channel_id=channel_id, + ), + remove_undefined(**data), + reason=reason, + t=ChannelData, + ) + ) + + async def start_thread_in_forum_channel( + self, + channel_id: int, + *, + name: str, + auto_archive_duration: Maybe[int] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + message: ForumThreadMessageParams, + applied_tags: Maybe[list[int]] = MISSING, + files: Maybe[list[File]] = MISSING, + reason: str | None = None, + ) -> ChannelData: + data = remove_undefined( + **{ + 'name': name, + 'auto_archive_duration': auto_archive_duration, + 'rate_limit_per_user': rate_limit_per_user, + 'message': message, + 'applied_tags': applied_tags, + } + ) + return cast( + ChannelData, + await self.request( + 'POST', + Route( + self, + '/channels/{channel_id}/threads', + channel_id=channel_id, + ), + data=data, + files=files, + reason=reason, + t=ChannelData, + ) + ) + + async def join_thread( + self, + channel_id: int, + ) -> None: + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/thread-members/@me', + channel_id=channel_id, + ), + t=None, + ) + ) + + async def add_thread_member( + self, + channel_id: int, + user_id: int, + ) -> None: + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/channels/{channel_id}/thread-members/{user_id}', + channel_id=channel_id, + user_id=user_id, + ), + t=None, + ) + ) + + async def leave_thread( + self, + channel_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/thread-members/@me', + channel_id=channel_id, + ), + t=None, + ) + ) + + async def remove_thread_member( + self, + channel_id: int, + user_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/channels/{channel_id}/thread-members/{user_id}', + channel_id=channel_id, + user_id=user_id, + ), + t=None, + ) + ) + + async def get_thread_member( + self, + channel_id: int, + user_id: int, + *, + with_member: Maybe[bool] = MISSING, + ) -> ThreadMemberData: + path = form_qs( + '/channels/{channel_id}/thread-members/{user_id}', + with_member=with_member, + ) + return cast( + ThreadMemberData, + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + user_id=user_id, + ), + t=ThreadMemberData, + ) + ) + + async def list_thread_members( + self, + channel_id: int, + *, + with_member: Maybe[bool] = MISSING, + after: Maybe[int] = MISSING, + limit: Maybe[int] = MISSING, + ) -> list[ThreadMemberData]: + path = form_qs( + '/channels/{channel_id}/thread-members', + with_member=with_member, + after=after, + limit=limit, + ) + return cast( + list[ThreadMemberData], + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + ), + t=list[ThreadMemberData], + ) + ) + + async def list_public_archived_threads( + self, + channel_id: int, + *, + before: Maybe[str] = MISSING, + limit: Maybe[int] = MISSING, + ) -> HasMoreListThreadsData: + path = form_qs( + '/channels/{channel_id}/threads/archived/public', + before=before, + limit=limit, + ) + return cast( + HasMoreListThreadsData, + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + ), + t=HasMoreListThreadsData, + ) + ) + + async def list_private_archived_threads( + self, + channel_id: int, + *, + before: Maybe[str] = MISSING, + limit: Maybe[int] = MISSING, + ) -> HasMoreListThreadsData: + path = form_qs( + '/channels/{channel_id}/threads/archived/private', + before=before, + limit=limit, + ) + return cast( + HasMoreListThreadsData, + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + ), + t=HasMoreListThreadsData, + ) + ) + + async def list_joined_private_archived_threads( + self, + channel_id: int, + *, + before: Maybe[str] = MISSING, + limit: Maybe[int] = MISSING, + ) -> HasMoreListThreadsData: + path = form_qs( + '/channels/{channel_id}/users/@me/threads/archived/private', + before=before, + limit=limit, + ) + return cast( + HasMoreListThreadsData, + await self.request( + 'GET', + Route( + self, + path, + channel_id=channel_id, + ), + t=HasMoreListThreadsData, + ) + ) + + # Emojis + async def list_guild_emojis(self, guild_id: int) -> list[EmojiData]: + return cast( + list[EmojiData], + await self.request( + 'GET', Route(self, '/guilds/{guild_id}/emojis', guild_id=guild_id), t=list[EmojiData], + ) + ) + + async def get_guild_emoji(self, guild_id: int, emoji_id: int) -> EmojiData: + return cast( + EmojiData, + await self.request( + 'GET', + Route( + self, + '/guilds/{guild_id}/emojis/{emoji_id}', + guild_id=guild_id, + emoji_id=emoji_id, + ), + t=EmojiData + ) + ) + + async def create_guild_emoji( + self, + guild_id: int, + *, + name: str, + image: File, + roles: list[int] | MissingEnum = MISSING, + reason: str | None = None, + ) -> EmojiData: + payload = { + 'name': name, + 'image': to_datauri(image), + 'roles': roles, + } + return cast( + EmojiData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/emojis', guild_id=guild_id), + remove_undefined(**payload), + reason=reason, + t=EmojiData + ) + ) + + async def modify_guild_emoji( + self, + guild_id: int, + emoji_id: int, + *, + name: str | MissingEnum = MISSING, + roles: list[int] | MissingEnum = MISSING, + reason: str | None = None, + ) -> EmojiData: + payload = { + 'name': name, + 'roles': roles, + } + return cast( + EmojiData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/emojis/{emoji_id}', + guild_id=guild_id, + emoji_id=emoji_id, + ), + remove_undefined(**payload), + reason=reason, + t=EmojiData, + ) + ) + + async def delete_guild_emoji( + self, + guild_id: int, + emoji_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/emojis/{emoji_id}', + guild_id=guild_id, + emoji_id=emoji_id, + ), + reason=reason, + t=None, + ) + ) + + # Guild + async def get_guild(self, guild_id: int, with_counts: bool = False) -> GuildData: + path = form_qs( + '/guilds/{guild_id}', + with_counts=with_counts, + ) + return cast( + GuildData, + await self.request( + 'GET', Route(self, path, guild_id=guild_id), t=GuildData, + ) + ) + + async def get_guild_preview(self, guild_id: int) -> GuildPreviewData: + return cast( + GuildPreviewData, + await self.request( + 'GET', Route(self, '/guilds/{guild_id}/preview', guild_id=guild_id), t=GuildPreviewData, + ) + ) + + async def modify_guild( + self, + guild_id: int, + *, + name: Maybe[str] = MISSING, + verification_level: Maybe[VerificationLevels | None] = MISSING, + default_message_notifications: Maybe[DefaultMessageNotificationLevels | None] = MISSING, + explicit_content_filter: Maybe[ExplicitContentFilterLevels | None] = MISSING, + afk_channel_id: Maybe[int | None] = MISSING, + afk_timeout: Maybe[int] = MISSING, + icon: Maybe[File | None] = MISSING, + owner_id: Maybe[int] = MISSING, + splash: Maybe[File | None] = MISSING, + discovery_splash: Maybe[File | None] = MISSING, + banner: Maybe[File | None] = MISSING, + system_channel_id: Maybe[int | None] = MISSING, + system_channel_flags: Maybe[int] = MISSING, + rules_channel_id: Maybe[int | None] = MISSING, + public_updates_channel_id: Maybe[int | None] = MISSING, + preferred_locale: Maybe[str | None] = MISSING, + features: Maybe[list[GuildFeatures]] = MISSING, + description: Maybe[str | None] = MISSING, + premium_progress_bar_enabled: Maybe[bool] = MISSING, + safety_alerts_channel_id: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> GuildData: + data = { + 'name': name, + 'verification_level': verification_level, + 'default_message_notifications': default_message_notifications, + 'explicit_content_filter': explicit_content_filter, + 'afk_channel_id': afk_channel_id, + 'afk_timeout': afk_timeout, + 'icon': to_datauri(icon) if icon else icon, + 'owner_id': owner_id, + 'splash': to_datauri(splash) if splash else splash, + 'discovery_splash': to_datauri(discovery_splash) if discovery_splash else discovery_splash, + 'banner': to_datauri(banner) if banner else banner, + 'system_channel_id': system_channel_id, + 'system_channel_flags': system_channel_flags, + 'rules_channel_id': rules_channel_id, + 'public_updates_channel_id': public_updates_channel_id, + 'preferred_locale': preferred_locale, + 'features': features, + 'description': description, + 'premium_progress_bar_enabled': premium_progress_bar_enabled, + 'safety_alerts_channel_id': safety_alerts_channel_id, + } + return cast( + GuildData, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}', guild_id=guild_id), + remove_undefined(**data), + reason=reason, + t=GuildData, + ) + ) + + async def delete_guild(self, guild_id: int) -> None: + return cast( + None, + await self.request( + 'DELETE', Route(self, '/guilds/{guild_id}', guild_id=guild_id), t=None, + ) + ) + + async def get_guild_channels(self, guild_id: int) -> list[ChannelData]: + return cast( + list[ChannelData], + await self.request( + 'GET', Route(self, '/guilds/{guild_id}/channels', guild_id=guild_id), t=list[ChannelData], + ) + ) + + async def create_guild_channel( + self, + guild_id: int, + *, + name: str, + type: Maybe[ChannelTypes | None], + topic: Maybe[str | None] = MISSING, + bitrate: Maybe[int | None] = MISSING, + user_limit: Maybe[int | None] = MISSING, + rate_limit_per_user: Maybe[int | None] = MISSING, + position: Maybe[int | None] = MISSING, + permission_overwrites: Maybe[list[PermissionOverwriteData] | None] = MISSING, + parent_id: Maybe[int | None] = MISSING, + nsfw: Maybe[bool | None] = MISSING, + rtc_region: Maybe[str | None] = MISSING, + video_quality_mode: Maybe[int | None] = MISSING, + default_auto_archive_duration: Maybe[int | None] = MISSING, + default_reaction_emoji: Maybe[DefaultReactionData | None] = MISSING, + available_tags: Maybe[list[ForumTagData] | None] = MISSING, + default_sort_order: Maybe[SortOrderTypes | None] = MISSING, + default_forum_layout: Maybe[ForumLayoutTypes | None] = MISSING, + default_thread_rate_limit_per_user: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> ChannelData: + data = remove_undefined( + name=name, + type=type, + topic=topic, + bitrate=bitrate, + user_limit=user_limit, + rate_limit_per_user=rate_limit_per_user, + position=position, + permission_overwrites=permission_overwrites, + parent_id=parent_id, + nsfw=nsfw, + rtc_region=rtc_region, + video_quality_mode=video_quality_mode, + default_auto_archive_duration=default_auto_archive_duration, + default_reaction_emoji=default_reaction_emoji, + available_tags=available_tags, + default_sort_order=default_sort_order, + default_forum_layout=default_forum_layout, + default_thread_rate_limit_per_user=default_thread_rate_limit_per_user, + ) + return cast( + ChannelData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/channels', guild_id=guild_id), + data, + reason=reason, + t=ChannelData, + ) + ) + + async def modify_guild_channel_positions( + self, + guild_id: int, + channels: list[ChannelPositionUpdateData], + ) -> None: + return cast( + None, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/channels', guild_id=guild_id), + channels, + t=None, + ) + ) + + async def list_active_guild_threads( + self, + guild_id: int, + ) -> list[ChannelData]: + return cast( + list[ChannelData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/threads/active', guild_id=guild_id), + t=list[ChannelData], + ) + ) + + async def get_guild_member(self, guild_id: int, user_id: int) -> GuildMemberData: + return cast( + GuildMemberData, + await self.request( + 'GET', + Route( + self, + '/guilds/{guild_id}/members/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + t=GuildMemberData, + ) + ) + + async def list_guild_members( + self, + guild_id: int, + *, + limit: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + ) -> list[GuildMemberData]: + path = form_qs( + '/guilds/{guild_id}/members', + limit=limit, + after=after, + ) + return cast( + list[GuildMemberData], + await self.request( + 'GET', + Route(self, path, guild_id=guild_id), + t=list[GuildMemberData], + ) + ) + + async def search_guild_members( + self, + guild_id: int, + query: str, + *, + limit: Maybe[int] = MISSING, + ) -> list[GuildMemberData]: + path = form_qs( + '/guilds/{guild_id}/members/search', + query=query, + limit=limit, + ) + return cast( + list[GuildMemberData], + await self.request( + 'GET', + Route(self, path, guild_id=guild_id), + t=list[GuildMemberData], + ) + ) + + async def add_guild_member( + self, + guild_id: int, + user_id: int, + *, + access_token: str, + nick: Maybe[str | None] = MISSING, + roles: Maybe[list[int]] = MISSING, + mute: Maybe[bool] = MISSING, + deaf: Maybe[bool] = MISSING, + reason: str | None = None, + ) -> GuildMemberData: + data = { + 'access_token': access_token, + 'nick': nick, + 'roles': roles, + 'mute': mute, + 'deaf': deaf, + } + return cast( + GuildMemberData, + await self.request( + 'PUT', + Route( + self, + '/guilds/{guild_id}/members/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + remove_undefined(**data), + reason=reason, + t=GuildMemberData, + ) + ) + + async def modify_guild_member( + self, + guild_id: int, + user_id: int, + *, + nick: Maybe[str | None] = MISSING, + roles: Maybe[list[int]] = MISSING, + mute: Maybe[bool | None] = MISSING, + deaf: Maybe[bool | None] = MISSING, + channel_id: Maybe[int | None] = MISSING, + communication_disabled_until: Maybe[datetime | None] = MISSING, + flags: Maybe[int | None] = MISSING, + reason: str | None = None, + ) -> GuildMemberData: + data = { + 'nick': nick, + 'roles': roles, + 'mute': mute, + 'deaf': deaf, + 'channel_id': channel_id, + 'communication_disabled_until': communication_disabled_until.isoformat() if communication_disabled_until else communication_disabled_until, + 'flags': flags, + } + return cast( + GuildMemberData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/members/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + remove_undefined(**data), + reason=reason, + t=GuildMemberData, + ) + ) + + async def modify_current_member( + self, + guild_id: int, + *, + nick: Maybe[str | None] = MISSING, + reason: str | None = None, + ) -> GuildMemberData: + data = { + 'nick': nick, + } + return cast( + GuildMemberData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/members/@me', + guild_id=guild_id, + ), + remove_undefined(**data), + reason=reason, + t=GuildMemberData, + ) + ) + + async def add_guild_member_role( + self, + guild_id: int, + user_id: int, + role_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', + guild_id=guild_id, + user_id=user_id, + role_id=role_id, + ), + reason=reason, + t=None, + ) + ) + + async def remove_guild_member_role( + self, + guild_id: int, + user_id: int, + role_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', + guild_id=guild_id, + user_id=user_id, + role_id=role_id, + ), + reason=reason, + t=None, + ) + ) + + async def remove_guild_member( + self, + guild_id: int, + user_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/members/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + reason=reason, + t=None, + ) + ) + + async def get_guild_bans( + self, + guild_id: int, + limit: Maybe[int] = MISSING, + before: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + ) -> list[BanData]: + path = form_qs( + '/guilds/{guild_id}/bans', + limit=limit, + before=before, + after=after, + ) + return cast( + list[BanData], + await self.request( + 'GET', Route(self, path, guild_id=guild_id), t=list[BanData], + ) + ) + + async def get_guild_ban( + self, + guild_id: int, + user_id: int, + ) -> BanData: + return cast( + BanData, + await self.request( + 'GET', + Route( + self, + '/guilds/{guild_id}/bans/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + t=BanData, + ) + ) + + async def create_guild_ban( + self, + guild_id: int, + user_id: int, + *, + delete_message_seconds: Maybe[int] = MISSING, + reason: str | None = None, + ) -> None: + data = { + 'delete_message_seconds': delete_message_seconds, + } + return cast( + None, + await self.request( + 'PUT', + Route( + self, + '/guilds/{guild_id}/bans/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + remove_undefined(**data), + reason=reason, + t=None, + ) + ) + + async def remove_guild_ban( + self, + guild_id: int, + user_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/bans/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + reason=reason, + t=None, + ) + ) + + async def get_guild_roles( + self, + guild_id: int, + ) -> list[RoleData]: + return cast( + list[RoleData], + await self.request( + 'GET', Route(self, '/guilds/{guild_id}/roles', guild_id=guild_id), t=list[RoleData], + ) + ) + + async def create_guild_role( + self, + guild_id: int, + *, + name: Maybe[str] = MISSING, + permissions: Maybe[str] = MISSING, + color: Maybe[int] = MISSING, + hoist: Maybe[bool] = MISSING, + icon: Maybe[File | None] = MISSING, + unicode_emoji: Maybe[str | None] = MISSING, + mentionable: Maybe[bool] = MISSING, + reason: str | None = None, + ) -> RoleData: + data = { + 'name': name, + 'permissions': permissions, + 'color': color, + 'hoist': hoist, + 'icon': to_datauri(icon) if icon else icon, + 'unicode_emoji': unicode_emoji, + 'mentionable': mentionable, + } + return cast( + RoleData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/roles', guild_id=guild_id), + remove_undefined(**data), + reason=reason, + t=RoleData, + ) + ) + + async def modify_guild_role_positions( + self, + guild_id: int, + roles: list[RolePositionUpdateData], + ) -> list[RoleData]: + return cast( + list[RoleData], + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/roles', guild_id=guild_id), + roles, + t=list[RoleData], + ) + ) + + async def modify_guild_role( + self, + guild_id: int, + role_id: int, + *, + name: Maybe[str] = MISSING, + permissions: Maybe[str] = MISSING, + color: Maybe[int] = MISSING, + hoist: Maybe[bool] = MISSING, + icon: Maybe[File | None] = MISSING, + unicode_emoji: Maybe[str | None] = MISSING, + mentionable: Maybe[bool] = MISSING, + reason: str | None = None, + ) -> RoleData: + data = { + 'name': name, + 'permissions': permissions, + 'color': color, + 'hoist': hoist, + 'icon': to_datauri(icon) if icon else icon, + 'unicode_emoji': unicode_emoji, + 'mentionable': mentionable, + } + return cast( + RoleData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/roles/{role_id}', + guild_id=guild_id, + role_id=role_id, + ), + remove_undefined(**data), + reason=reason, + t=RoleData, + ) + ) + + async def modify_guild_mfa_level( + self, + guild_id: int, + *, + level: MFALevels, + reason: str | None = None, + ) -> MFALevelResponse: + data = { + 'mfa_level': level, + } + return cast( + MFALevelResponse, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}', guild_id=guild_id), + data, + reason=reason, + t=MFALevelResponse, + ) + ) + + async def delete_guild_role( + self, + guild_id: int, + role_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/roles/{role_id}', + guild_id=guild_id, + role_id=role_id, + ), + reason=reason, + t=None, + ) + ) + + async def get_guild_prune_count( + self, + guild_id: int, + *, + days: Maybe[int] = MISSING, + include_roles: Maybe[list[int]] = MISSING, + ) -> PruneCountResponse: + path = form_qs( + '/guilds/{guild_id}/prune', + days=days, + include_roles=include_roles, + ) + return cast( + PruneCountResponse, + await self.request( + 'GET', + Route(self, path, guild_id=guild_id), + t=PruneCountResponse, + ) + ) + + async def begin_guild_prune( + self, + guild_id: int, + *, + days: Maybe[int] = MISSING, + compute_prune_count: Maybe[bool] = MISSING, + include_roles: Maybe[list[int]] = MISSING, + reason: str | None = None, + ) -> PruneCountResponse: + data = { + 'days': days, + 'compute_prune_count': compute_prune_count, + 'include_roles': include_roles, + } + return cast( + PruneCountResponse, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/prune', guild_id=guild_id), + remove_undefined(**data), + reason=reason, + t=PruneCountResponse, + ) + ) + + async def get_guild_voice_regions( + self, + guild_id: int, + ) -> list[VoiceRegionData]: + return cast( + list[VoiceRegionData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/regions', guild_id=guild_id), + t=list[VoiceRegionData], + ) + ) + + async def get_guild_invites( + self, + guild_id: int, + ) -> list[InviteData]: + return cast( + list[InviteData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/invites', guild_id=guild_id), + t=list[InviteData], + ) + ) + + async def get_guild_integrations( + self, + guild_id: int, + ) -> list[IntegrationData]: + return cast( + list[IntegrationData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/integrations', guild_id=guild_id), + t=list[IntegrationData], + ) + ) + + async def delete_guild_integration( + self, + guild_id: int, + integration_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/integrations/{integration_id}', + guild_id=guild_id, + integration_id=integration_id, + ), + reason=reason, + t=None, + ) + ) + + async def get_guild_widget_settings( + self, + guild_id: int, + ) -> GuildWidgetSettingsData: + return cast( + GuildWidgetSettingsData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/widget', guild_id=guild_id), + t=GuildWidgetSettingsData, + ) + ) + + async def modify_guild_widget( + self, + guild_id: int, + *, + enabled: bool, + channel_id: int | None, + reason: str | None = None, + ) -> GuildWidgetSettingsData: + data = { + 'enabled': enabled, + 'channel_id': channel_id, + } + return cast( + GuildWidgetSettingsData, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/widget', guild_id=guild_id), + data, + reason=reason, + t=GuildWidgetSettingsData, + ) + ) + + async def get_guild_widget( + self, + guild_id: int, + ) -> GuildWidgetData: + return cast( + GuildWidgetData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/widget.json', guild_id=guild_id), + t=GuildWidgetData, + ) + ) + + async def get_guild_vanity_url( + self, + guild_id: int, + ) -> VanityURLData: + return cast( + VanityURLData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/vanity-url', guild_id=guild_id), + t=VanityURLData, + ) + ) + + async def get_guild_widget_image( + self, + guild_id: int, + *, + style: Maybe[Literal["shield", "banner1", "banner2", "banner3", "banner4"]] = MISSING, + ) -> bytes: + path = form_qs( + '/guilds/{guild_id}/widget.png', + style=style, + ) + return cast( + bytes, + await self.request( + 'GET', + Route(self, path, guild_id=guild_id), + t=bytes, + ) + ) + + async def get_guild_welcome_screen( + self, + guild_id: int, + ) -> WelcomeScreenData: + return cast( + WelcomeScreenData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), + t=WelcomeScreenData, + ) + ) + + async def modify_guild_welcome_screen( + self, + guild_id: int, + *, + enabled: Maybe[bool | None] = MISSING, + welcome_channels: Maybe[list[WelcomeChannelData] | None] = MISSING, + description: Maybe[str | None] = MISSING, + reason: str | None = None, + ) -> WelcomeScreenData: + data = { + 'enabled': enabled, + 'welcome_channels': welcome_channels, + 'description': description, + } + return cast( + WelcomeScreenData, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), + remove_undefined(**data), + reason=reason, + t=WelcomeScreenData, + ) + ) + + async def get_guild_onboarding( + self, + guild_id: int, + ) -> GuildOnboardingData: + return cast( + GuildOnboardingData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/onboarding', guild_id=guild_id), + t=GuildOnboardingData, + ) + ) + + async def modify_guild_onboarding( + self, + guild_id: int, + *, + prompts: list[GuildOnboardingPromptsData], + default_channel_ids: list[int], + enabled: bool, + mode: GuildOnboardingModes, + reason: str | None = None, + ) -> GuildOnboardingData: + data = { + 'prompts': prompts, + 'default_channel_ids': default_channel_ids, + 'enabled': enabled, + 'mode': mode, + } + return cast( + GuildOnboardingData, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/onboarding', guild_id=guild_id), + remove_undefined(**data), + reason=reason, + t=GuildOnboardingData, + ) + ) + + async def modify_current_user_voice_state( + self, + guild_id: int, + *, + channel_id: Maybe[int] = MISSING, + suppress: Maybe[bool] = MISSING, + request_to_speak_timestamp: Maybe[datetime | None] = MISSING, + ) -> None: + data = { + 'channel_id': channel_id, + 'suppress': suppress, + 'request_to_speak_timestamp': request_to_speak_timestamp.isoformat() if request_to_speak_timestamp else request_to_speak_timestamp, + } + return cast( + None, + await self.request( + 'PATCH', + Route(self, '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id), + remove_undefined(**data), + t=None, + ) + ) + + async def modify_user_voice_state( + self, + guild_id: int, + user_id: int, + *, + channel_id: Maybe[int] = MISSING, + suppress: Maybe[bool] = MISSING, + ) -> None: + data = { + 'channel_id': channel_id, + 'suppress': suppress, + } + return cast( + None, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/voice-states/{user_id}', + guild_id=guild_id, + user_id=user_id, + ), + remove_undefined(**data), + t=None, + ) + ) + + # Guild Scheduled Event + async def list_scheduled_events_for_guild( + self, + guild_id: int, + *, + with_user_count: Maybe[bool] = MISSING, + ) -> list[GuildScheduledEventData]: + path = form_qs( + '/guilds/{guild_id}/scheduled-events', + with_user_count=with_user_count, + ) + return cast( + list[GuildScheduledEventData], + await self.request( + 'GET', + Route(self, path, guild_id=guild_id), + t=list[GuildScheduledEventData], + ) + ) + + async def create_guild_scheduled_event( + self, + guild_id: int, + *, + channel_id: int, + entity_metadata: Maybe[GuildScheduledEventEntityMetadataData] = MISSING, + name: str, + privacy_level: GuildScheduledEventPrivacyLevels, + scheduled_start_time: datetime, + scheduled_end_time: Maybe[datetime] = MISSING, + description: Maybe[str] = MISSING, + entity_type: GuildScheduledEventEntityTypes, + image: Maybe[File | None] = MISSING, + reason: str | None = None, + ) -> GuildScheduledEventData: + data = { + 'channel_id': channel_id, + 'entity_metadata': entity_metadata, + 'name': name, + 'privacy_level': privacy_level, + 'scheduled_start_time': scheduled_start_time.isoformat(), + 'scheduled_end_time': scheduled_end_time.isoformat() if scheduled_end_time else scheduled_end_time, + 'description': description, + 'entity_type': entity_type, + 'image': to_datauri(image) if image else image, + } + return cast( + GuildScheduledEventData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/scheduled-events', guild_id=guild_id), + data, + reason=reason, + t=GuildScheduledEventData, + ) + ) + + async def get_guild_scheduled_event( + self, + guild_id: int, + event_id: int, + *, + with_user_count: Maybe[bool] = MISSING, + ) -> GuildScheduledEventData: + path = form_qs( + '/guilds/{guild_id}/scheduled-events/{event_id}', + with_user_count=with_user_count, + ) + return cast( + GuildScheduledEventData, + await self.request( + 'GET', + Route( + self, + path, + guild_id=guild_id, + event_id=event_id, + ), + t=GuildScheduledEventData, + ) + ) + + async def modify_guild_scheduled_event( + self, + guild_id: int, + event_id: int, + *, + channel_id: Maybe[int] = MISSING, + entity_metadata: Maybe[GuildScheduledEventEntityMetadataData | None] = MISSING, + name: Maybe[str] = MISSING, + privacy_level: Maybe[GuildScheduledEventPrivacyLevels] = MISSING, + scheduled_start_time: Maybe[datetime] = MISSING, + scheduled_end_time: Maybe[datetime] = MISSING, + description: Maybe[str | None] = MISSING, + entity_type: Maybe[GuildScheduledEventEntityTypes] = MISSING, + status: Maybe[GuildScheduledEventStatus] = MISSING, + image: Maybe[File | None] = MISSING, + reason: str | None = None, + ) -> GuildScheduledEventData: + data = { + 'channel_id': channel_id, + 'entity_metadata': entity_metadata, + 'name': name, + 'privacy_level': privacy_level, + 'scheduled_start_time': scheduled_start_time.isoformat() if scheduled_start_time else scheduled_start_time, + 'scheduled_end_time': scheduled_end_time.isoformat() if scheduled_end_time else scheduled_end_time, + 'description': description, + 'entity_type': entity_type, + 'status': status, + 'image': to_datauri(image) if image else image, + } + return cast( + GuildScheduledEventData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/scheduled-events/{event_id}', + guild_id=guild_id, + event_id=event_id, + ), + remove_undefined(**data), + reason=reason, + t=GuildScheduledEventData, + ) + ) + + async def delete_guild_scheduled_event( + self, + guild_id: int, + event_id: int, + *, + reason: str | None = None, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/scheduled-events/{event_id}', + guild_id=guild_id, + event_id=event_id, + ), + reason=reason, + t=None, + ) + ) + + async def get_guild_scheduled_event_users( + self, + guild_id: int, + event_id: int, + *, + limit: Maybe[int] = MISSING, + with_member: Maybe[bool] = MISSING, + before: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + ) -> list[GuildScheduledEventUserData]: + path = form_qs( + '/guilds/{guild_id}/scheduled-events/{event_id}/users', + limit=limit, + with_member=with_member, + before=before, + after=after, + ) + return cast( + list[GuildScheduledEventUserData], + await self.request( + 'GET', + Route( + self, + path, + guild_id=guild_id, + event_id=event_id, + ), + t=list[GuildScheduledEventUserData], + ) + ) + + # Guild Template + async def get_guild_template( + self, + template_code: str, + ) -> GuildTemplateData: + return cast( + GuildTemplateData, + await self.request( + 'GET', + Route(self, '/guilds/templates/{template_code}', template_code=template_code), + t=GuildTemplateData, + ) + ) + + async def create_guild_from_guild_template( + self, + template_code: str, + *, + name: str, + icon: Maybe[File | None] = MISSING, + ) -> GuildData: + data = { + 'name': name, + 'icon': to_datauri(icon) if icon else icon, + } + return cast( + GuildData, + await self.request( + 'POST', + Route(self, '/guilds/templates/{template_code}', template_code=template_code), + remove_undefined(**data), + t=GuildData, + ) + ) + + async def get_guild_templates( + self, + guild_id: int, + ) -> list[GuildTemplateData]: + return cast( + list[GuildTemplateData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/templates', guild_id=guild_id), + t=list[GuildTemplateData], + ) + ) + + async def create_guild_template( + self, + guild_id: int, + *, + name: str, + description: Maybe[str | None] = MISSING, + ) -> GuildTemplateData: + data = { + 'name': name, + 'description': description, + } + return cast( + GuildTemplateData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/templates', guild_id=guild_id), + remove_undefined(**data), + t=GuildTemplateData, + ) + ) + + async def sync_guild_template( + self, + guild_id: int, + template_code: str, + ) -> GuildTemplateData: + return cast( + GuildTemplateData, + await self.request( + 'PUT', + Route( + self, + '/guilds/{guild_id}/templates/{template_code}', + guild_id=guild_id, + template_code=template_code, + ), + t=GuildTemplateData, + ) + ) + + async def modify_guild_template( + self, + guild_id: int, + template_code: str, + *, + name: Maybe[str] = MISSING, + description: Maybe[str | None] = MISSING, + ) -> GuildTemplateData: + data = { + 'name': name, + 'description': description, + } + return cast( + GuildTemplateData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/templates/{template_code}', + guild_id=guild_id, + template_code=template_code, + ), + remove_undefined(**data), + t=GuildTemplateData, + ) + ) + + async def delete_guild_template( + self, + guild_id: int, + template_code: str, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/templates/{template_code}', + guild_id=guild_id, + template_code=template_code, + ), + t=None, + ) + ) + + # Invites + async def get_invite( + self, + invite_code: str, + *, + with_counts: Maybe[bool] = MISSING, + with_expiration: Maybe[bool] = MISSING, + guild_scheduled_event_id: Maybe[int] = MISSING, + ) -> InviteData: + path = form_qs( + '/invites/{invite_code}', + with_counts=with_counts, + with_expiration=with_expiration, + guild_scheduled_event_id=guild_scheduled_event_id, + ) + return cast( + InviteData, + await self.request( + 'GET', + Route(self, path, invite_code=invite_code), + t=InviteData, + ) + ) + + async def delete_invite( + self, + invite_code: str, + *, + reason: str | None = None, + ) -> InviteData: + return cast( + InviteData, + await self.request( + 'DELETE', + Route(self, '/invites/{invite_code}', invite_code=invite_code), + reason=reason, + t=InviteData, + ) + ) + + # Stage Instance + async def create_stage_instance( + self, + channel_id: int, + *, + topic: str, + privacy_level: Maybe[StageInstancePrivacyLevels] = MISSING, + send_start_notification: Maybe[bool] = MISSING, + guild_scheduled_event_id: Maybe[int] = MISSING, + reason: str | None = None, + ) -> StageInstanceData: + data = { + 'channel_id': channel_id, + 'topic': topic, + 'privacy_level': privacy_level, + 'send_start_notification': send_start_notification, + 'guild_scheduled_event_id': guild_scheduled_event_id, + } + return cast( + StageInstanceData, + await self.request( + 'POST', + Route(self, '/stage-instances'), + data, + reason=reason, + t=StageInstanceData, + ) + ) + + async def get_stage_instance( + self, + channel_id: int, + ) -> StageInstanceData: + return cast( + StageInstanceData, + await self.request( + 'GET', + Route(self, '/stage-instances/{channel_id}', channel_id=channel_id), + t=StageInstanceData, + ) + ) + + async def modify_stage_instance( + self, + channel_id: int, + *, + topic: Maybe[str] = MISSING, + privacy_level: Maybe[StageInstancePrivacyLevels] = MISSING, + reason: str | None = None, + ) -> StageInstanceData: + data = { + 'topic': topic, + 'privacy_level': privacy_level, + } + return cast( + StageInstanceData, + await self.request( + 'PATCH', + Route(self, '/stage-instances/{channel_id}', channel_id=channel_id), + data, + reason=reason, + t=StageInstanceData, + ) + ) + + async def delete_stage_instance( + self, + channel_id: int, + *, + reason: str | None = None, + ) -> StageInstanceData: + return cast( + StageInstanceData, + await self.request( + 'DELETE', + Route(self, '/stage-instances/{channel_id}', channel_id=channel_id), + reason=reason, + t=StageInstanceData, + ) + ) + + # Stickers + async def get_sticker( + self, + sticker_id: int, + ) -> StickerData: + return cast( + StickerData, + await self.request( + 'GET', + Route(self, '/stickers/{sticker_id}', sticker_id=sticker_id), + t=StickerData, + ) + ) + + async def list_sticker_packs( + self, + ) -> list[StickerPackData]: + return cast( + list[StickerPackData], + await self.request( + 'GET', + Route(self, '/sticker-packs'), + t=list[StickerPackData], + ) + ) + + async def list_guild_stickers( + self, + guild_id: int, + ) -> list[StickerData]: + return cast( + list[StickerData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/stickers', guild_id=guild_id), + t=list[StickerData], + ) + ) + + async def get_guild_sticker( + self, + guild_id: int, + sticker_id: int, + ) -> StickerData: + return cast( + StickerData, + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id), + t=StickerData, + ) + ) + + async def create_guild_sticker( + self, + guild_id: int, + *, + name: str, + description: str, + tags: str, + file: File, + reason: str | None = None, + ) -> StickerData: + data = [ + {"name": "name", "value": name}, + {"name": "description", "value": description}, + {"name": "tags", "value": tags}, + { + "name": "file", "value": file.file.read(), + "filename": file.filename, "content_type": "application/octet-stream" + }, + ] + return cast( + StickerData, + await self.request( + 'POST', + Route(self, '/guilds/{guild_id}/stickers', guild_id=guild_id), + form=data, + files=[file], + add_files=False, + reason=reason, + t=StickerData, + ) + ) + + async def modify_guild_sticker( + self, + guild_id: int, + sticker_id: int, + *, + name: Maybe[str] = MISSING, + description: Maybe[str | None] = MISSING, + tags: Maybe[str] = MISSING, + reason: str | None = None, + ) -> StickerData: + data = { + 'name': name, + 'description': description, + 'tags': tags, + } + return cast( + StickerData, + await self.request( + 'PATCH', + Route( + self, + '/guilds/{guild_id}/stickers/{sticker_id}', + guild_id=guild_id, + sticker_id=sticker_id, + ), + remove_undefined(**data), + reason=reason, + t=StickerData, + ) + ) + + async def delete_guild_sticker( + self, + guild_id: int, + sticker_id: int, + *, + reason: str | None = None, + ) -> StickerData: + return cast( + StickerData, + await self.request( + 'DELETE', + Route( + self, + '/guilds/{guild_id}/stickers/{sticker_id}', + guild_id=guild_id, + sticker_id=sticker_id, + ), + reason=reason, + t=StickerData, + ) + ) + + # Users + async def get_current_user( + self, + ) -> UserData: + return cast( + UserData, + await self.request( + 'GET', + Route(self, '/users/@me'), + t=UserData, + ) + ) + + async def get_user( + self, + user_id: int, + ) -> UserData: + return cast( + UserData, + await self.request( + 'GET', + Route(self, '/users/{user_id}', user_id=user_id), + t=UserData, + ) + ) + + async def modify_current_user( + self, + *, + username: Maybe[str] = MISSING, + avatar: Maybe[File | None] = MISSING, + ) -> UserData: + data = { + 'username': username, + 'avatar': to_datauri(avatar) if avatar else avatar, + } + return cast( + UserData, + await self.request( + 'PATCH', + Route(self, '/users/@me'), + remove_undefined(**data), + t=UserData, + ) + ) + + async def get_current_user_guilds( + self, + *, + before: Maybe[int] = MISSING, + after: Maybe[int] = MISSING, + limit: Maybe[int] = 200, + with_counts: Maybe[bool] = False, + ) -> list[PartialGuildData]: + path = form_qs( + '/users/@me/guilds', + before=before, + after=after, + limit=limit, + with_counts=with_counts, + ) + return cast( + list[PartialGuildData], + await self.request( + 'GET', + Route(self, path), + t=list[PartialGuildData], + ) + ) + + async def get_current_user_guild_member( + self, + guild_id: int, + ) -> GuildMemberData: + return cast( + GuildMemberData, + await self.request( + 'GET', + Route(self, '/users/@me/guilds/{guild_id}', guild_id=guild_id), + t=GuildMemberData, + ) + ) + + async def leave_guild( + self, + guild_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route(self, '/users/@me/guilds/{guild_id}', guild_id=guild_id), + t=None, + ) + ) + + async def create_dm( + self, + *, + recipient_id: int, + ) -> ChannelData: + data = { + 'recipient_id': recipient_id, + } + return cast( + ChannelData, + await self.request( + 'POST', + Route(self, '/users/@me/channels'), + data, + t=ChannelData, + ) + ) + + async def create_group_dm( + self, + *, + access_tokens: list[str], + nicks: dict[int, str], + ) -> ChannelData: + data = { + 'access_tokens': access_tokens, + 'nicks': nicks, + } + return cast( + ChannelData, + await self.request( + 'POST', + Route(self, '/users/@me/channels'), + data, + t=ChannelData, + ) + ) + + async def get_current_user_connections( + self, + ) -> list[ConnectionData]: + return cast( + list[ConnectionData], + await self.request( + 'GET', + Route(self, '/users/@me/connections'), + t=list[ConnectionData], + ) + ) + + async def update_current_user_application_role_connection( + self, + application_id: int, + access_token: str, + *, + platform_name: Maybe[str] = MISSING, + platform_username: Maybe[str] = MISSING, + metadata: Maybe[dict[str, int | datetime | bool]] = MISSING, + ) -> ApplicationRoleConnectionData: + data = { + 'platform_name': platform_name, + 'platform_username': platform_username, + 'metadata': metadata, + } + return cast( + ApplicationRoleConnectionData, + await self.request( + 'PUT', + Route(self, '/users/@me/applications/{application_id}/role-connection', application_id=application_id), + remove_undefined(**data), + headers={"Authorization": "Bearer " + access_token}, + t=ApplicationRoleConnectionData, + ) + ) + + async def list_voice_regions( + self, + ) -> list[VoiceRegionData]: + return cast( + list[VoiceRegionData], + await self.request( + 'GET', + Route(self, '/voice/regions'), + t=list[VoiceRegionData], + ) + ) + + # Webhooks + async def create_webhook( + self, + channel_id: int, + *, + name: str, + avatar: Maybe[File | None] = MISSING, + ) -> WebhookData: + data = { + 'name': name, + 'avatar': to_datauri(avatar) if avatar else avatar, + } + return cast( + WebhookData, + await self.request( + 'POST', + Route(self, '/channels/{channel_id}/webhooks', channel_id=channel_id), + remove_undefined(**data), + t=WebhookData, + ) + ) + + async def get_channel_webhooks( + self, + channel_id: int, + ) -> list[WebhookData]: + return cast( + list[WebhookData], + await self.request( + 'GET', + Route(self, '/channels/{channel_id}/webhooks', channel_id=channel_id), + t=list[WebhookData], + ) + ) + + async def get_guild_webhooks( + self, + guild_id: int, + ) -> list[WebhookData]: + return cast( + list[WebhookData], + await self.request( + 'GET', + Route(self, '/guilds/{guild_id}/webhooks', guild_id=guild_id), + t=list[WebhookData], + ) + ) + + async def get_webhook( + self, + webhook_id: int, + ) -> WebhookData: + return cast( + WebhookData, + await self.request( + 'GET', + Route(self, '/webhooks/{webhook_id}', webhook_id=webhook_id), + t=WebhookData, + ) + ) + + async def get_webhook_with_token( + self, + webhook_id: int, + webhook_token: str, + ) -> WebhookData: + return cast( + WebhookData, + await self.request( + 'GET', + Route( + self, '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=webhook_token + ), + t=WebhookData, + ) + ) + + async def modify_webhook( + self, + webhook_id: int, + *, + name: Maybe[str] = MISSING, + avatar: Maybe[File | None] = MISSING, + channel_id: Maybe[int] = MISSING, + ) -> WebhookData: + data = { + 'name': name, + 'avatar': to_datauri(avatar) if avatar else avatar, + 'channel_id': channel_id, + } + return cast( + WebhookData, + await self.request( + 'PATCH', + Route(self, '/webhooks/{webhook_id}', webhook_id=webhook_id), + remove_undefined(**data), + t=WebhookData, + ) + ) + + async def modify_webhook_with_token( + self, + webhook_id: int, + webhook_token: str, + *, + name: Maybe[str] = MISSING, + avatar: Maybe[File | None] = MISSING, + ) -> WebhookData: + data = { + 'name': name, + 'avatar': to_datauri(avatar) if avatar else avatar, + } + return cast( + WebhookData, + await self.request( + 'PATCH', + Route( + self, '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=webhook_token + ), + remove_undefined(**data), + t=WebhookData, + ) + ) + + async def delete_webhook( + self, + webhook_id: int, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route(self, '/webhooks/{webhook_id}', webhook_id=webhook_id), + t=None, + ) + ) + + async def delete_webhook_with_token( + self, + webhook_id: int, + webhook_token: str, + ) -> None: + return cast( + None, + await self.request( + 'DELETE', + Route( + self, '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=webhook_token + ), + t=None, + ) + ) + + async def execute_webhook( + self, + webhook_id: int, + webhook_token: str, + *, + wait: Maybe[bool] = MISSING, + thread_id: Maybe[int] = MISSING, + content: Maybe[str] = MISSING, + username: Maybe[str] = MISSING, + avatar_url: Maybe[str] = MISSING, + tts: Maybe[bool] = MISSING, + embeds: Maybe[list[EmbedData]] = MISSING, + allowed_mentions: Maybe[AllowedMentionsData] = MISSING, + components: Maybe[list[ComponentData]] = MISSING, + files: Maybe[list[File]] = MISSING, + attachments: Maybe[list[PartialAttachmentData]] = MISSING, + flags: Maybe[int] = MISSING, + thread_name: Maybe[str] = MISSING, + applied_tags: Maybe[list[int]] = MISSING, + ) -> MessageData: + path = form_qs( + '/webhooks/{webhook_id}/{webhook_token}', + wait=wait, + thread_id=thread_id, + ) + data = remove_undefined( + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + embeds=embeds, + allowed_mentions=allowed_mentions, + components=components, + attachments=attachments, + flags=flags, + thread_name=thread_name, + applied_tags=applied_tags, + ) + return cast( + MessageData, + await self.request( + 'POST', + Route(self, path, webhook_id=webhook_id, webhook_token=webhook_token), + data, + files=files, + t=MessageData, + ) + ) + + async def get_webhook_message( + self, + webhook_id: int, + webhook_token: str, + message_id: int, + *, + thread_id: Maybe[int] = MISSING, + ) -> MessageData: + path = form_qs( + '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', + thread_id=thread_id, + ) + return cast( + MessageData, + await self.request( + 'GET', + Route(self, path, webhook_id=webhook_id, webhook_token=webhook_token, message_id=message_id), + t=MessageData, + ) + ) + + async def edit_webhook_message( + self, + webhook_id: int, + webhook_token: str, + message_id: int, + *, + thread_id: Maybe[int] = MISSING, + content: Maybe[str | None] = MISSING, + embeds: Maybe[list[EmbedData] | None] = MISSING, + allowed_mentions: Maybe[AllowedMentionsData | None] = MISSING, + components: Maybe[list[ComponentData] | None] = MISSING, + files: Maybe[list[File] | None] = MISSING, + attachments: Maybe[list[PartialAttachmentData] | None] = MISSING, + ) -> MessageData: + path = form_qs( + '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', + thread_id=thread_id, + ) + data = remove_undefined( + content=content, + embeds=embeds, + allowed_mentions=allowed_mentions, + components=components, + attachments=attachments, + ) + return cast( + MessageData, + await self.request( + 'PATCH', + Route(self, path, webhook_id=webhook_id, webhook_token=webhook_token, message_id=message_id), + data, + files=files, + t=MessageData, + ) + ) + + async def delete_webhook_message( + self, + webhook_id: int, + webhook_token: str, + message_id: int, + *, + thread_id: int, + ) -> None: + path = form_qs( + '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', + thread_id=thread_id, + ) + return cast( + None, + await self.request( + 'DELETE', + Route(self, path, webhook_id=webhook_id, webhook_token=webhook_token, message_id=message_id), + t=None, + ) + ) diff --git a/pycord/gateway/passthrough.py b/pycord/internal/reserver.py similarity index 93% rename from pycord/gateway/passthrough.py rename to pycord/internal/reserver.py index 21fd8269..7eb8e07c 100644 --- a/pycord/gateway/passthrough.py +++ b/pycord/internal/reserver.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,12 +18,13 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE +# SOFTWARE. + import time from asyncio import AbstractEventLoop, Future, get_running_loop -class PassThrough: +class Reserver: def __init__(self, concurrency: int, per: float | int) -> None: self.concurrency: int = concurrency self.per: float | int = per @@ -32,7 +34,7 @@ def __init__(self, concurrency: int, per: float | int) -> None: self.loop: AbstractEventLoop = get_running_loop() self.pending_reset: bool = False - async def __aenter__(self) -> 'PassThrough': + async def __aenter__(self) -> "Reserver": while self.current == 0: future = self.loop.create_future() self._reserved.append(future) diff --git a/pycord/internal/shard.py b/pycord/internal/shard.py new file mode 100644 index 00000000..ebdd7d91 --- /dev/null +++ b/pycord/internal/shard.py @@ -0,0 +1,276 @@ +# MIT License +# +# Copyright (c) 2023 Pycord +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import annotations + +import asyncio +import logging +import zlib +from asyncio import Task +from platform import system +from random import random +from typing import TYPE_CHECKING, Any + +from aiohttp import BasicAuth, ClientConnectionError, ClientConnectorError, WSMsgType + +from ..errors import DisallowedIntents, InvalidAuth, ShardingRequired +from ..task_descheduler import tasks +from ..utils import dumps, loads +from .reserver import Reserver + +if TYPE_CHECKING: + from ..state.core import State + +_log = logging.getLogger(__name__) + +RESUMABLE: list[int] = [ + 4000, + 4001, + 4002, + 4003, + 4005, + 4007, + 4008, + 4009, +] + + +class Shard: + URL_FMT = "{base}/?v={version}&encoding=json&compress=zlib-stream" + ZLIB_SUFFIX = b"\x00\x00\xff\xff" + + def __init__( + self, + state: State, + shard_id: int, + shard_total: int, + version: int, + proxy: str | None, + proxy_auth: BasicAuth | None, + ) -> None: + self._state: "State" = state + self.version = version + self.shard_id = shard_id + self.shard_total = shard_total + + # While Discord's rate limit is actually 120, + # leaving out 10 gives Shards more safety in terms + # of heart beating + self.rate_limiter = Reserver(110, 60) + self.open = False + + self._hb_task: Task | None = None + self._recv_task: Task | None = None + self._proxy = proxy + self._proxy_auth = proxy_auth + self._hello_received = asyncio.Event() + self._resume_gateway_url = None + + def log(self, level: int, msg: str) -> None: + _log.log(level, f"shard:{self.shard_id}: {msg}") + + async def connect(self, resume: bool = False) -> None: + self.open = False + self._hello_received.clear() + self._inflator = zlib.decompressobj() + self._hb_task: Task | None = None + self._recv_task: Task | None = None + + if not resume: + self.session_id = None + self._sequence = 0 + self._resume_gateway_url = None + + try: + async with self._state.shard_rate_limit: + self.log(logging.INFO, "attempting connection to gateway") + self._ws = await self._state.http._session.ws_connect( + url=self.URL_FMT.format( + version=self.version, base=self._resume_gateway_url + ) + if resume and self._resume_gateway_url + else self.URL_FMT.format( + version=self.version, base="wss://gateway.discord.gg" + ), + proxy=self._proxy, + proxy_auth=self._proxy_auth, + ) + except (ClientConnectionError, ClientConnectorError): + self.log( + logging.ERROR, + "connection errors led to failure in connecting to the gateway, trying again in 10 seconds", + ) + await asyncio.sleep(10) + await self.connect(resume=resume) + return + else: + self.log(logging.INFO, "attempt successful") + self.open = True + + self._recv_task = asyncio.create_task(self._recv()) + + if not resume: + await self._hello_received.wait() + await self.send_identify() + else: + await self.send_resume() + + async def _recv(self) -> None: + async for message in self._ws: + if message.type == WSMsgType.CLOSED: + break + elif message.type == WSMsgType.BINARY: + if len(message.data) < 4 or message.data[-4:] != self.ZLIB_SUFFIX: + continue + + try: + text_coded = self._inflator.decompress(message.data).decode("utf-8") + except Exception as e: + # while being an edge case, the data could sometimes be corrupted. + self.log( + logging.ERROR, + f"failed to decompress gateway data {message.data}:{e}", + ) + continue + + self.log(logging.DEBUG, f"received message {text_coded}") + + data: dict[str, Any] = loads(text_coded) + + self._sequence = data.get("s") + + async with tasks() as tg: + tg[asyncio.create_task(self.on_receive(data))] + + self.handle_close(self._ws.close_code) + + async def on_receive(self, data: dict[str, Any]) -> None: + op: int = data.get("op") + d: dict[str, Any] | int | None = data.get("d") + t: str | None = data.get("t") + + if op == 0: + if t == "READY": + self.session_id = d["session_id"] + self._resume_gateway_url = d["resume_gateway_url"] + await self._state.cache["users"].upsert(d["user"]["id"], d["user"]) + self._state.user_id = d["user"]["id"] + await self._state.event_manager.push(t, d) + elif op == 1: + await self._ws.send_str(dumps({"op": 1, "d": self._sequence})) + elif op == 7: + await self._ws.close(code=1002) + await self.connect(resume=True) + return + elif op == 10: + self._heartbeat_interval = d["heartbeat_interval"] / 1000 + + self._hb_task = asyncio.create_task(self._heartbeat_loop()) + self._hello_received.set() + + async def _heartbeat_loop(self) -> None: + jitter = True + + while not self._ws.closed: + if jitter: + jitter = False + await asyncio.sleep(self._heartbeat_interval + random()) + else: + await asyncio.sleep(self._heartbeat_interval) + + self.log(logging.DEBUG, "attempting heartbeat") + + try: + await self._ws.send_str(dumps({"op": 1, "d": self._sequence})) + except ConnectionResetError: + self.log( + logging.ERROR, + f"failed to send heartbeat due to connection reset, attempting reconnection", + ) + self._receive_task.cancel() + if not self._ws.closed: + await self._ws.close(code=1008) + await self.connect(bool(self._resume_gateway_url)) + return + + async def send(self, data: dict[str, Any]) -> None: + async with self.rate_limiter: + d = dumps(data) + self.log(logging.DEBUG, f"sending {d}") + await self._ws.send_str(d) + + async def send_identify(self) -> None: + self.log(logging.INFO, "shard is identifying") + + await self.send( + { + "op": 2, + "d": { + "token": self._state._token, + "properties": { + "os": system(), + "browser": "pycord", + "device": "pycord", + }, + "compress": True, + "large_threshold": self._state.large_threshold, + "shard": [self.shard_id, self.shard_total], + "intents": self._state.intents, + }, + } + ) + + async def send_resume(self) -> None: + await self.send( + { + "op": 6, + "d": { + "token": self._state._token, + "session_id": self.session_id, + "seq": self._sequence, + }, + } + ) + + async def handle_close(self, code: int | None) -> None: + self.log(logging.ERROR, f"shard socket closed with code {code}") + if self._hb_task and not self._hb_task.done(): + self._hb_task.cancel() + if code in RESUMABLE: + await self.connect(True) + elif code is None: + await self.connect(True) + else: + if code == 4004: + raise InvalidAuth("Authentication used in gateway is invalid") + elif code == 4011: + raise ShardingRequired("Discord is requiring you shard your bot") + elif code == 4014: + raise DisallowedIntents( + "You aren't allowed to carry a privileged intent wanted" + ) + + if code > 4000 or code == 4000: + await self.connect(resume=False) + else: + # the connection most likely died + await self.connect(resume=True) diff --git a/pycord/invite.py b/pycord/invite.py index 50655a98..7acb4c18 100644 --- a/pycord/invite.py +++ b/pycord/invite.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,73 +18,62 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE + from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING -from .application import Application -from .channel import Channel from .enums import InviteTargetType -from .guild import Guild -from .missing import MISSING, Maybe, MissingEnum -from .scheduled_event import ScheduledEvent -from .types import Invite as DiscordInvite, InviteMetadata as DiscordInviteMetadata +from .application import Application +from .channel import Channel, channel_factory +from .guild import Guild, GuildMember +from .guild_scheduled_event import GuildScheduledEvent +from .missing import Maybe, MISSING from .user import User if TYPE_CHECKING: - from .state import State + from discord_typings import InviteData, InviteStageInstanceData + from .state import State -class InviteMetadata: - def __init__(self, data: DiscordInviteMetadata) -> None: - self.uses: int = data['uses'] - self.max_uses: int = data['max_uses'] - self.max_age: int = data['max_age'] - self.temporary: bool = data['temporary'] - self.created_at: datetime = datetime.fromisoformat(data['created_at']) +__all__ = ( + "Invite", + "InviteStageInstance", +) class Invite: - def __init__(self, data: DiscordInvite, state: State) -> None: - self.code: str = data['code'] - self.guild: Guild | MissingEnum = ( - Guild(data['guild'], state) if data.get('guild') is not None else MISSING - ) - self.channel: Channel | None = ( - Channel(data['channel'], state) if data.get('channel') is not None else None - ) - self.inviter: User | MissingEnum = ( - User(data['inviter'], state) if data.get('inviter') is not None else MISSING - ) - self.target_type: int | MissingEnum = ( - InviteTargetType(data['target_type']) - if data.get('target_type') is not None - else MISSING - ) - self.target_user: User | MissingEnum = ( - User(data['target_user'], state) - if data.get('target_user') is not None - else MISSING - ) - self.target_application: Application | MissingEnum = ( - Application(data['target_application'], state) - if data.get('target_application') is not None - else MISSING - ) - self.approximate_presence_count: int | MissingEnum = data.get( - 'approximate_presence_count', MISSING - ) - self.approximate_member_count: int | MissingEnum = data.get( - 'approximate_member_count', MISSING - ) - self.expires_at: datetime | None = ( - datetime.fromisoformat(data['expires_at']) - if data.get('expires_at') is not None - else data.get('expires_at', MISSING) - ) - self.guild_scheduled_event: ScheduledEvent | MissingEnum = ( - ScheduledEvent(data['guild_scheduled_event'], state) - if data.get('guild_scheduled_event') is not None - else MISSING - ) + def __init__(self, data: InviteData, state: State) -> None: + self._state: "State" = state + self._update(data) + + def _update(self, data: InviteData) -> None: + self.code: str = data["code"] + self.guild: Maybe[Guild] = Guild(data["guild"], self._state) if (data.get("guild")) else MISSING + self.channel: Channel | None = channel_factory(cnl, self._state) if (cnl := data.get("channel")) else None + self.inviter: Maybe[User] = User(inviter, self._state) if (inviter := data.get("inviter")) else MISSING + self.target_type: Maybe[InviteTargetType] = InviteTargetType(data.get("target_type")) if ( + data.get("target_type")) else MISSING + self.target_user: Maybe[User] = User(target_user, self._state) if ( + target_user := data.get("target_user")) else MISSING + self.target_application: Maybe[Application] = Application(target_application, self._state) if ( + target_application := data.get("target_application")) else MISSING + self.approximate_presence_count: Maybe[int] = data.get("approximate_presence_count", MISSING) + self.approximate_member_count: Maybe[int] = data.get("approximate_member_count", MISSING) + self.expires_at: Maybe[datetime | None] = datetime.fromisoformat(expires) if (expires := data.get( + "expires_at", MISSING + )) not in (None, MISSING) else expires + self.stage_instance: Maybe[InviteStageInstance] = InviteStageInstance(stage_instance, self._state) if ( + stage_instance := data.get("stage_instance")) else MISSING + self.guild_scheduled_event: Maybe[GuildScheduledEvent] = GuildScheduledEvent( + guild_scheduled_event, self._state + ) if (guild_scheduled_event := data.get("guild_scheduled_event")) else MISSING + + +class InviteStageInstance: + def __init__(self, data: InviteStageInstanceData, state: State) -> None: + self.members: list[GuildMember] = [GuildMember(member, state) for member in data["members"]] + self.participant_count: int = data["participant_count"] + self.speaker_count: int = data["speaker_count"] + self.topic: str = data["topic"] diff --git a/pycord/media.py b/pycord/media.py deleted file mode 100644 index 10ed7351..00000000 --- a/pycord/media.py +++ /dev/null @@ -1,134 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -import re -from typing import TYPE_CHECKING, Any - -from .enums import StickerFormatType, StickerType -from .missing import MISSING, Maybe, MissingEnum -from .role import Role -from .snowflake import Snowflake -from .types import ( - Attachment as DiscordAttachment, - Emoji as DiscordEmoji, - Sticker as DiscordSticker, - StickerItem as DiscordStickerItem, - User as DiscordUser, -) -from .user import User - -if TYPE_CHECKING: - from .state import State - - -class Emoji: - _EMOJI_NAME_REGEX = re.compile( - r'a)?:?(?P\w+):(?P[0-9]{13,20})>?' - ) - - def __init__(self, data: DiscordEmoji, state: State) -> None: - self._state: State = state - self.id: Snowflake | None = ( - Snowflake(data['id']) if data['id'] is not None else None - ) - self.name: str | None = data.get('name') - self._roles: list[Snowflake] = [ - Snowflake(role) for role in data.get('roles', []) - ] - self.roles: list[Role] = [] - self._user: DiscordUser | MissingEnum = data.get('user', MISSING) - self.user: MissingEnum | User = ( - User(self._user, state) if self._user is not MISSING else MISSING - ) - self.require_colons: MissingEnum | bool = data.get('require_colons', MISSING) - self.managed: MissingEnum | bool = data.get('managed', MISSING) - self.animated: MissingEnum | bool = data.get('animated', MISSING) - self.available: MissingEnum | bool = data.get('available', MISSING) - - def _inject_roles(self, roles: list[Role]) -> None: - for role in roles: - if role.id in self._roles: - self.roles.append(role) - - def _partial(self) -> dict[str, Any]: - return {'name': self.name, 'id': self.id, 'animated': self.animated} - - @classmethod - def _from_str(cls, string: str, state: State) -> 'Emoji': - match = cls._EMOJI_NAME_REGEX.match(string) - - if match: - grps = match.groupdict() - return cls( - { - 'animated': bool(grps['animated']), - 'id': Snowflake(grps['id']), - 'name': grps['name'], - }, - state, - ) - - # assumes this is unicode - return cls({'name': string, 'id': None, 'animated': False}, state) - - -class StickerItem: - def __init__(self, data: DiscordStickerItem) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.format_type: StickerFormatType = StickerFormatType(data['format_type']) - - -class Sticker: - def __init__(self, data: DiscordSticker, state: State) -> None: - self.id: Snowflake | None = Snowflake(data['id']) - self.pack_id: Snowflake | None = ( - Snowflake(data.get('pack_id')) if data.get('pack_id') is not None else None - ) - self.name: str = data['name'] - self.description: str | None = data['description'] - self.tags: list[str] = data['tags'].split(',') - self.type: StickerType = StickerType(data['type']) - self.format_type: StickerFormatType = StickerFormatType(data['format_type']) - self.available: bool | MissingEnum = data.get('available', MISSING) - self.guild_id: Snowflake | None = ( - Snowflake(data['guild_id']) if data['guild_id'] is not None else None - ) - self._user: DiscordUser | MissingEnum = data.get('user', MISSING) - self.user: MissingEnum | User = ( - User(self._user, state) if self._user is not MISSING else MISSING - ) - self.sort_value: MissingEnum | int = data.get('sort_value', MISSING) - - -class Attachment: - def __init__(self, data: DiscordAttachment, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.filename: str = data['filename'] - self.description: str | MissingEnum = data.get('description', MISSING) - self.content_type: str | MissingEnum = data.get('content_type', MISSING) - self.size: int = data.get('size') - self.url: str = data.get('url') - self.proxy_url: str = data.get('proxy_url') - self.height: int | None | MissingEnum = data.get('height', MISSING) - self.width: int | None | MissingEnum = data.get('width', MISSING) - self.ephemeral: bool | MissingEnum = data.get('ephemeral', MISSING) diff --git a/pycord/member.py b/pycord/member.py deleted file mode 100644 index a8a3f721..00000000 --- a/pycord/member.py +++ /dev/null @@ -1,227 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING - -from .flags import MemberFlags, Permissions -from .role import Role -from .snowflake import Snowflake - -if TYPE_CHECKING: - from .state import State - -from .missing import MISSING, Maybe, MissingEnum -from .pages import Page -from .pages.paginator import Paginator -from .types import GuildMember -from .user import User - - -class Member: - __slots__ = ( - '_state', - '_guild_id', - 'user', - 'nick', - '_avatar', - 'roles', - 'joined_at', - 'premium_since', - 'deaf', - 'mute', - 'pending', - 'permissions', - 'communication_disabled_until', - ) - - def __init__( - self, data: GuildMember, state: State, *, guild_id: Snowflake | None = None - ) -> None: - self._state: State = state - self._guild_id: Snowflake | None = guild_id or None - self.user: User | MissingEnum = ( - User(data.get('user'), state) if data.get('user') is not None else MISSING - ) - self.nick: str | None | MissingEnum = data.get('nick', MISSING) - self._avatar: str | None | MissingEnum = data.get('avatar', MISSING) - self.roles: list[Snowflake] = [Snowflake(s) for s in data['roles']] - self.joined_at: datetime = datetime.fromisoformat(data['joined_at']) - self.premium_since: None | MissingEnum | datetime = ( - datetime.fromisoformat(data.get('premium_since')) - if data.get('premium_since', MISSING) not in [MISSING, None] - else data.get('premium_since', MISSING) - ) - self.deaf: bool | MissingEnum = data.get('deaf', MISSING) - self.mute: bool | MissingEnum = data.get('mute', MISSING) - self.pending: MissingEnum | bool = data.get('pending', MISSING) - self.permissions: Permissions | MissingEnum = ( - Permissions.from_value(data.get('permissions')) - if data.get('permissions', MISSING) is not MISSING - else MISSING - ) - self.communication_disabled_until: None | MissingEnum | datetime = ( - datetime.fromisoformat(data.get('communication_disabled_until')) - if data.get('communication_disabled_until', MISSING) not in [MISSING, None] - else data.get('communication_disabled_until', MISSING) - ) - - async def edit( - self, - *, - nick: str | None | MissingEnum = MISSING, - roles: list[Snowflake] | MissingEnum = MISSING, - mute: bool | MissingEnum = MISSING, - deaf: bool | MissingEnum = MISSING, - channel_id: Snowflake | None | MissingEnum = MISSING, - communication_disabled_until: datetime | None | MissingEnum = MISSING, - flags: MemberFlags | None | MissingEnum = MISSING, - reason: str | None = None, - ) -> Member: - communication_disabled_until = ( - communication_disabled_until.isoformat() - if communication_disabled_until - else communication_disabled_until - ) - data = await self._state.http.modify_guild_member( - self._guild_id, - self.user.id, - nick=nick, - roles=(roles or []) if roles is not MISSING else roles, - mute=mute, - deaf=deaf, - channel_id=channel_id, - communication_disabled_until=communication_disabled_until, - flags=flags.value if flags else flags, - reason=reason, - ) - return Member(data, self._state, guild_id=self._guild_id) - - async def add_role( - self, - role: Role, - *, - reason: str | None = None, - ) -> None: - """Adds a role to the member. - - Parameters - ---------- - role: :class:`Role` - The role to add. - reason: :class:`str` | None - The reason for adding the role. Shows up in the audit log. - """ - await self._state.http.add_guild_member_role( - self._guild_id, - self.id, - role.id, - reason=reason, - ) - - async def remove_role( - self, - role: Role, - *, - reason: str | None = None, - ) -> None: - """Removes a role from the member. - - Parameters - ---------- - role: :class:`Role` - The role to remove. - reason: :class:`str` | None - The reason for removing the role. Shows up in the audit log. - """ - await self._state.http.remove_guild_member_role( - self._guild_id, - self.id, - role.id, - reason=reason, - ) - - async def kick(self, *, reason: str | None = None): - """Kicks the member from the guild. - - Parameters - ---------- - reason: :class:`str` | None - The reason for kicking the member. Shows up in the audit log. - """ - await self._state.http.remove_guild_member( - self._guild_id, - self.id, - reason=reason, - ) - - -class MemberPage(Page[Member]): - def __init__(self, member: Member) -> None: - self.value = member - - -class MemberPaginator(Paginator[MemberPage]): - def __init__( - self, - state: State, - guild_id: Snowflake, - *, - limit: int = 1, - after: datetime | None = None, - ) -> None: - super().__init__() - self._state: State = state - self.guild_id: Snowflake = guild_id - self.limit: int | None = limit - if after: - self.last_id: Snowflake = Snowflake.from_datetime(after) - else: - self.last_id: MissingEnum = MISSING - self.done = False - - async def fill(self): - if self._previous_page is None or self._previous_page[0] >= len(self._pages): - if self.done: - raise StopAsyncIteration - limit = min(self.limit, 1000) if self.limit else 1000 - if self.limit is not None: - self.limit -= limit - data = await self._state.http.list_guild_members( - self.guild_id, - limit=limit, - after=self.last_id, - ) - if len(data) < limit or self.limit <= 0: - self.done = True - if not data: - raise StopAsyncIteration - for member in data: - self.add_page( - MemberPage(Member(member, self._state, guild_id=self.guild_id)) - ) - - async def forward(self): - await self.fill() - value = await super().forward() - self.last_id = value.user.id - return value diff --git a/pycord/message.py b/pycord/message.py index 6c03f3d1..4cfa1c88 100644 --- a/pycord/message.py +++ b/pycord/message.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,469 +18,187 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations - -import asyncio -from datetime import datetime -from typing import TYPE_CHECKING - -from .application import Application -from .embed import Embed -from .enums import ChannelType, InteractionType, MessageActivityType, MessageType -from .errors import ComponentException -from .flags import MessageFlags -from .media import Attachment, Emoji, Sticker, StickerItem -from .member import Member -from .missing import MISSING, Maybe, MissingEnum -from .role import Role -from .snowflake import Snowflake -from .types import ( - AllowedMentions as DiscordAllowedMentions, - ChannelMention as DiscordChannelMention, - Message as DiscordMessage, - MessageActivity as DiscordMessageActivity, - MessageInteraction as DiscordMessageInteraction, - MessageReference as DiscordMessageReference, - Reaction as DiscordReaction, -) -from .user import User - -if TYPE_CHECKING: - from .channel import ( - AnnouncementChannel, - AnnouncementThread, - CategoryChannel, - DirectoryChannel, - DMChannel, - ForumChannel, - StageChannel, - TextChannel, - Thread, - VoiceChannel, - ) - from .state import State - from .ui.house import House - - -class ChannelMention: - __slots__ = ('id', 'guild_id', 'type', 'name') - - def __init__(self, data: DiscordChannelMention) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self.type: ChannelType = ChannelType(data['type']) - self.name: str = data['name'] - - -class Reaction: - __slots__ = ('count', 'me', 'emoji') - - def __init__(self, data: DiscordReaction) -> None: - self.count: int = data['count'] - self.me: bool = data['me'] - self.emoji: Emoji = Emoji(data['emoji']) - - -class MessageActivity: - __slots__ = ('type', 'party_id') - - def __init__(self, data: DiscordMessageActivity) -> None: - self.type: MessageActivityType = MessageActivityType(data['type']) - self.party_id: MissingEnum | str = data.get('party_id', MISSING) -class MessageReference: - __slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists') - - def __init__(self, data: DiscordMessageReference) -> None: - self.message_id: Snowflake | MissingEnum = ( - Snowflake(data.get('message_id')) - if data.get('message_id') is not None - else MISSING - ) - self.channel_id: Snowflake | MissingEnum = ( - Snowflake(data.get('channel_id')) - if data.get('channel_id') is not None - else MISSING - ) - self.guild_id: Snowflake | MissingEnum = ( - Snowflake(data.get('guild_id')) - if data.get('guild_id') is not None - else MISSING - ) - self.fail_if_not_exists: bool | MissingEnum = data.get( - 'fail_if_not_exists', MISSING - ) - - # refactor so user can initialize - def __init__( - self, - message_id: Snowflake | MissingEnum = MISSING, - channel_id: Snowflake | MissingEnum = MISSING, - guild_id: Snowflake | MissingEnum = MISSING, - fail_if_not_exists: bool | MissingEnum = MISSING, - ) -> None: - self.message_id: Snowflake | MissingEnum = message_id - self.channel_id: Snowflake | MissingEnum = channel_id - self.guild_id: Snowflake | MissingEnum = guild_id - self.fail_if_not_exists: bool | MissingEnum = fail_if_not_exists - - def to_dict(self) -> DiscordMessageReference: - data = {} - if self.message_id is not MISSING: - data['message_id'] = self.message_id - if self.channel_id is not MISSING: - data['channel_id'] = self.channel_id - if self.guild_id is not MISSING: - data['guild_id'] = self.guild_id - if self.fail_if_not_exists is not MISSING: - data['fail_if_not_exists'] = self.fail_if_not_exists - return data - - @classmethod - def from_dict(cls, data: DiscordMessageReference) -> MessageReference: - return cls( - message_id=data.get('message_id', MISSING), - channel_id=data.get('channel_id', MISSING), - guild_id=data.get('guild_id', MISSING), - ) +from datetime import datetime +from typing import Self +from discord_typings import AttachmentData, ChannelMentionData, MessageData +from pycord.asset import Asset +from pycord.enums import ChannelType +from pycord.flags import AttachmentFlags +from pycord.missing import MISSING, Maybe +from pycord.mixins import Identifiable, Snowflake +from pycord.state import State +from pycord.user import User -class MessageInteraction: - __slots__ = ('id', 'type', 'name', 'user', 'member') - def __init__(self, data: DiscordMessageInteraction, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.type: InteractionType = InteractionType(data['type']) - self.name: str = data['name'] - self.user: User = User(data['user'], state) - self.member: Member | MissingEnum = ( - Member(data.get('member'), state) - if data.get('member') is not None - else MISSING - ) +class Message(Identifiable): + def __init__(self, data: "MessageData", state: "State") -> None: + self._state = state + self._update(data) + + def _update(self, data: "MessageData") -> None: + self.id: int = int(data["id"]) + self.channel_id: int = int(data["channel_id"]) + self.author: User = User(data=data["author"], state=self._state) + self.content: str = data["content"] + self.timestamp: datetime = datetime.fromisoformat(data["timestamp"]) + self.edited_timestamp: datetime | None = datetime.fromisoformat(edited_ts) if ( + edited_ts := data.get("edited_timestamp")) else None + self.tts: bool = data["tts"] + self.mention_everyone: bool = data["mention_everyone"] + self.mentions: list[User] = [User(data=mention, state=self._state) for mention in data["mentions"]] + self.mention_roles: list[int] = [int(mention) for mention in data["mention_roles"]] + self.mention_channels: list[ChannelMention] = [ChannelMention(data=mention, state=self._state) for mention in + data["mention_channels"]] + self.attachments: list[Attachment] = [Attachment(data=attachment, state=self._state) for attachment in + data["attachments"]] + # TODO: embeds, reactions, nonce, pinned, webhook_id, type, activity, application, + # application_id, message_reference, flags, referenced_message, interaction, thread, + # components, sticker_items, stickers, position, role_subcription_data, resolved + + +class ChannelMention(Identifiable): + __slots__ = ("id", "guild_id", "type", "name") + + def __init__(self, data: "ChannelMentionData", state: "State") -> None: + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + self.type: ChannelType = ChannelType(data["type"]) + self.name: str = data["name"] class AllowedMentions: - __slots__ = ('everyone', 'roles', 'users', 'replied_user') + __slots__ = ( + "parse", + "role_ids", + "user_ids", + "replied_user", + ) def __init__( self, *, - everyone: bool = False, - roles: bool | list[Role] = False, - users: bool | list[User] = False, - replied_user: bool = False, + role_mentions: bool = True, + user_mentions: bool = True, + everyone: bool = True, + roles: list[Snowflake] | None = None, + users: list[Snowflake] | None = None, + replied_user: bool = True ) -> None: - self.everyone: bool = everyone - self.roles: bool | list[Role] = roles - self.users: bool | list[User] = users + if role_mentions and roles: + raise ValueError("Cannot specify roles when role mentions are enabled") + if user_mentions and users: + raise ValueError("Cannot specify users when user mentions are enabled") + self.parse: list[str] = [] + if role_mentions: + self.parse.append("roles") + if user_mentions: + self.parse.append("users") + if everyone: + self.parse.append("everyone") + self.role_ids: list[int] = [r.id for r in roles or []] + self.user_ids: list[int] = [r.id for r in users or []] self.replied_user: bool = replied_user - def to_dict(self) -> DiscordAllowedMentions: - data = {} - parse = [] - if self.everyone is True: - parse.append('everyone') - if self.roles is True: - parse.append('roles') - elif isinstance(self.roles, list): - data['roles'] = [r.id for r in self.roles] - if self.users is True: - parse.append('users') - elif isinstance(self.users, list): - data['users'] = [u.id for u in self.users] - if self.replied_user is True: - data['replied_user'] = True - if parse: - data['parse'] = parse - return data - + @classmethod + def none(cls) -> Self: + return cls(role_mentions=False, user_mentions=False, everyone=False, replied_user=False) -class Message: - __slots__ = ( - '_state', - 'id', - 'channel_id', - 'author', - 'content', - 'timestamp', - 'edited_timestamp', - 'tts', - 'mentions', - 'mention_roles', - 'attachments', - 'embeds', - 'reactions', - 'nonce', - 'pinned', - 'webhook_id', - 'type', - 'activity', - 'application', - 'application_id', - 'reference', - 'flags', - 'referenced_message', - 'interaction', - 'thread', - 'sticker_items', - 'stickers', - 'position', - 'channel', - ) + @classmethod + def all(cls) -> Self: + return cls(role_mentions=True, user_mentions=True, everyone=True, replied_user=True) - def __init__(self, data: DiscordMessage, state: State) -> None: - self._state = state - self.id: Snowflake = Snowflake(data['id']) - self.channel_id: Snowflake = Snowflake(data['channel_id']) - self.author: User = User(data['author'], state=state) - self.content: str = data['content'] - self.timestamp: datetime = datetime.fromisoformat(data['timestamp']) - self.edited_timestamp: datetime | None = ( - datetime.fromisoformat(data['edited_timestamp']) - if data['edited_timestamp'] is not None - else None - ) - self.tts: bool = data['tts'] - self.mentions: list[User] = [User(d, state) for d in data['mentions']] - self.mention_roles: list[Snowflake] = [ - Snowflake(i) for i in data['mention_roles'] - ] - self.mention_channels: list[ChannelMention] = [ - ChannelMention(d) for d in data.get('mention_channels', []) - ] - self.attachments: list[Attachment] = [ - Attachment(a, state) for a in data['attachments'] - ] - self.embeds: list[Embed] = [Embed._from_data(e) for e in data['embeds']] - self.reactions: list[Reaction] = [ - Reaction(r) for r in data.get('reactions', []) - ] - self.nonce: MissingEnum | int | str = data.get('nonce', MISSING) - self.pinned: bool = data['pinned'] - self.webhook_id: Snowflake = ( - Snowflake(data.get('webhook_id')) - if data.get('webhook_id') is not None - else MISSING - ) - self.type: MessageType = MessageType(data['type']) - self.activity: MessageActivity | MissingEnum = ( - MessageActivity(data.get('activity')) - if data.get('activity') is not None - else MISSING - ) - self.application: Application | MissingEnum = ( - Application(data.get('application')) - if data.get('application') is not None - else MISSING - ) - self.application_id: Snowflake | MissingEnum = ( - Snowflake(data.get('application_id')) - if data.get('application_id') is not None - else MISSING - ) - self.reference: MessageReference | MissingEnum = ( - MessageReference.from_dict(data.get('message_reference')) - if data.get('message_reference') is not None - else MISSING - ) - self.flags: MessageFlags | MissingEnum = ( - MessageFlags.from_value(data.get('flags')) - if data.get('flags') is not None - else MISSING - ) - self.referenced_message: Message | MissingEnum = ( - Message(data.get('referenced_message'), state) - if data.get('referenced_message') is not None - else MISSING - ) - self.interaction: MessageInteraction | MissingEnum = ( - MessageInteraction(data.get('interaction'), state) - if data.get('interaction') is not None - else MISSING - ) - self.thread: Thread | MissingEnum = ( - Thread(data.get('thread'), state=state) - if data.get('thread') is not None - else MISSING - ) - # TODO: Work on components - # self.components - self.sticker_items: list[StickerItem] = [ - StickerItem(si) for si in data.get('sticker_items', []) - ] - self.stickers: list[Sticker] = [Sticker(s) for s in data.get('stickers', [])] - self.position: MissingEnum | int = data.get('position', MissingEnum) - asyncio.create_task(self._retreive_channel()) + @classmethod + def no_reply(cls) -> Self: + return cls(replied_user=False) - async def _retreive_channel(self) -> None: - exists = await (self._state.store.sift('channels')).get_without_parents( - self.channel_id - ) + @property + def role_mentions(self) -> bool: + return "roles" in self.parse - if exists: - self.channel: TextChannel | DMChannel | VoiceChannel | CategoryChannel | AnnouncementChannel | AnnouncementThread | Thread | StageChannel | DirectoryChannel | ForumChannel = exists[ - 1 - ] + @role_mentions.setter + def role_mentions(self, value: bool) -> None: + if value is self.role_mentions: + return + if value: + self.parse.append("roles") else: - self.channel: TextChannel | DMChannel | VoiceChannel | CategoryChannel | AnnouncementChannel | AnnouncementThread | Thread | StageChannel | DirectoryChannel | ForumChannel | None = ( - None - ) + self.parse.remove("roles") - def _modify_from_cache(self, **keys) -> None: - # this is a bit finnicky but works well - for k, v in keys.items(): - if hasattr(self, k): - setattr(self, k, v) + @property + def user_mentions(self) -> bool: + return "users" in self.parse - match k: - case 'edited_timestamp': - # edited timestamp can't be none on edited messages, I think? - self.edited_timestamp = datetime.fromisoformat(v) - case 'mentions': - self.mentions: list[User] = [User(d, self._state) for d in v] - case 'mention_roles': - self.mention_roles: list[Snowflake] = [Snowflake(i) for i in v] - case 'mention_channels': - self.mention_channels: list[ChannelMention] = [ - ChannelMention(d) for d in v - ] - case 'attachments': - self.attachments: list[Attachment] = [ - Attachment(a, self._state) for a in v - ] - case 'embeds': - self.embeds: list[Embed] = [Embed._from_data(e) for e in v] - case 'reactions': - self.reactions: list[Reaction] = [Reaction(r) for r in v] - case 'flags': - self.flags: MessageFlags = MessageFlags.from_value(v) - - async def crosspost(self) -> Message: - data = await self._state.http.crosspost_message(self.channel_id, self.id) - return Message(data, self._state) - - async def add_reaction(self, emoji: Emoji | str) -> None: - if isinstance(emoji, Emoji): - emoji = {'id': emoji.id, 'name': emoji.name} - await self._state.http.create_reaction( - self.channel_id, - self.id, - emoji, - ) - - async def remove_reaction(self, emoji: Emoji | str) -> None: - if isinstance(emoji, Emoji): - emoji = {'id': emoji.id, 'name': emoji.name} - await self._state.http.delete_own_reaction( - self.channel_id, - self.id, - emoji, - ) - - async def remove_user_reaction(self, emoji: Emoji | str, user: User) -> None: - if isinstance(emoji, Emoji): - emoji = {'id': emoji.id, 'name': emoji.name} - await self._state.http.delete_user_reaction( - self.channel_id, - self.id, - emoji, - user.id, - ) - - async def get_reactions( - self, - emoji: Emoji | str, - *, - after: Snowflake | MissingEnum = MISSING, - limit: int = 25, - ) -> list[User]: - if isinstance(emoji, Emoji): - emoji = {'id': emoji.id, 'name': emoji.name} - data = await self._state.http.get_reactions( - self.channel_id, - self.id, - emoji, - after=after, - limit=limit, - ) - return [User(d, self._state) for d in data] - - async def remove_all_reactions(self, *, emoji: Emoji | str | None = None) -> None: - if emoji is not None: - if isinstance(emoji, Emoji): - emoji = {'id': emoji.id, 'name': emoji.name} - await self._state.http.delete_all_reactions_for_emoji( - self.channel_id, - self.id, - emoji, - ) + @user_mentions.setter + def user_mentions(self, value: bool) -> None: + if value is self.user_mentions: return - await self._state.http.delete_all_reactions( - self.channel_id, - self.id, - ) - - async def edit( - self, - *, - content: str | None | MissingEnum = MISSING, - embeds: list[Embed] | None | MissingEnum = MISSING, - allowed_mentions: AllowedMentions | None | MissingEnum = MISSING, - attachments: list[Attachment] | None | MissingEnum = MISSING, - flags: MessageFlags | None | MissingEnum = MISSING, - houses: list[House] | MissingEnum = MISSING, - ) -> Message: - if houses: - if len(houses) > 5: - raise ComponentException('Cannot have over five houses at once') - - components = [(house.action_row())._to_dict() for house in houses] - - for house in houses: - self._state.sent_house(house) + if value: + self.parse.append("users") else: - components = MISSING + self.parse.remove("users") - data = await self._state.http.edit_message( - self.channel_id, - self.id, - content=content, - embeds=embeds, - allowed_mentions=allowed_mentions, - attachments=attachments, - components=components, - flags=flags, - ) - return Message(data, self._state) + @property + def everyone(self) -> bool: + return "everyone" in self.parse - async def delete(self, *, reason: str | None = None) -> None: - await self._state.http.delete_message(self.channel_id, self.id, reason=reason) + @everyone.setter + def everyone(self, value: bool) -> None: + if value is self.everyone: + return + if value: + self.parse.append("everyone") + else: + self.parse.remove("everyone") - async def reply(self, *args, **kwargs) -> Message: - kwargs['message_reference'] = MessageReference( - message_id=self.id, - channel_id=self.channel_id, - ) - return await self.channel.send(*args, **kwargs) + def to_dict(self) -> dict[str, list[int] | bool]: + return { + "parse": self.parse, + "roles": self.role_ids, + "users": self.user_ids, + "replied_user": self.replied_user + } - async def pin(self) -> None: - await self._state.http.pin_message(self.channel_id, self.id) - async def unpin(self) -> None: - await self._state.http.unpin_message(self.channel_id, self.id) +class Attachment(Identifiable): + __slots__ = ( + "_state", + "id", + "filename", + "description", + "size", + "url", + "proxy_url", + "height", + "width", + "ephemeral", + "duration_secs", + "waveform", + "flags", + ) - async def create_thread( - self, - *, - name: str, - auto_archive_duration: int | MissingEnum = MISSING, - rate_limit_per_user: int | MissingEnum = MISSING, - ) -> Thread | AnnouncementThread: - return await self.channel.create_thread( - self, - name=name, - auto_archive_duration=auto_archive_duration, - rate_limit_per_user=rate_limit_per_user, - ) + def __init__(self, data: "AttachmentData", state: "State") -> None: + self._state = state + self.id: int = int(data["id"]) + self.filename: str = data["filename"] + self.description: Maybe[str] = data.get("description", MISSING) + self.size: int = data["size"] + self.url: str = data["url"] + self.proxy_url: str = data["proxy_url"] + self.height: Maybe[int | None] = data.get("height", MISSING) + self.width: Maybe[int | None] = data.get("width", MISSING) + self.ephemeral: Maybe[bool] = data.get("ephemeral", MISSING) + self.duration_secs: Maybe[int] = data.get("duration_secs", MISSING) + self.waveform: Maybe[str] = data.get("waveform", MISSING) + self.flags: Maybe[AttachmentFlags] = AttachmentFlags.from_value(flags) if ( + flags := data.get("flags")) else MISSING + + @property + def asset(self) -> Asset: + return Asset(self._state, url=self.url) + + @property + def proxy_asset(self) -> Asset: + return Asset(self._state, url=self.proxy_url) diff --git a/pycord/message_iterator.py b/pycord/message_iterator.py deleted file mode 100644 index 93d31128..00000000 --- a/pycord/message_iterator.py +++ /dev/null @@ -1,33 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from .message import Message -from .pages import Page -from .pages.paginator import Paginator - - -class MessagePage(Page[Message]): - def __init__(self, message: Message) -> None: - self.value = message - - -# Paginator but typed for MessagePage -class MessagePaginator(Paginator[MessagePage]): - ... diff --git a/pycord/missing.py b/pycord/missing.py index d00e2c9a..405a860a 100644 --- a/pycord/missing.py +++ b/pycord/missing.py @@ -22,9 +22,9 @@ # SOFTWARE from enum import Enum, auto -from typing import Literal, TypeVar, Union +from typing import Literal, TypeAlias, TypeVar, Union -T = TypeVar('T') +T = TypeVar("T") class MissingEnum(Enum): @@ -39,5 +39,4 @@ def __bool__(self) -> Literal[False]: An instance of `.missing.MissingEnum` for purposes of code use. """ - -Maybe = Union[T, Literal[MissingEnum.MISSING]] +Maybe: TypeAlias = Union[T, Literal[MissingEnum.MISSING]] diff --git a/pycord/snowflake.py b/pycord/mixins.py similarity index 54% rename from pycord/snowflake.py rename to pycord/mixins.py index 0c2d6e69..30885f38 100644 --- a/pycord/snowflake.py +++ b/pycord/mixins.py @@ -18,37 +18,48 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -"""Implementation of Discord's Snowflake ID""" -from __future__ import annotations +from datetime import datetime -from datetime import datetime, timezone +__all__ = ( + "Identifiable", +) -from .utils import DISCORD_EPOCH +from typing import Protocol -class Snowflake(int): - @property - def timestamp(self) -> datetime: - return datetime.fromtimestamp( - ((self >> 22) + DISCORD_EPOCH) / 1000, tz=timezone.utc - ) +class Snowflake(Protocol): + id: int - @property - def worker_id(self) -> int: - return (self & 0x3E0000) >> 17 - @property - def process_id(self) -> int: - return (self & 0x1F000) >> 12 +class Identifiable(Snowflake): + __slots__ = () - @property - def increment(self) -> int: - return self & 0xFFF + id: int + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and other.id == self.id + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) def __hash__(self) -> int: - return self >> 22 + return self.id >> 22 + + +class Messageable(Identifiable): + async def get_messages(self, *, around: datetime = None, before: int = None, after: int = None, limit: int = 50): + # TODO: implement + raise NotImplementedError + + async def get_message(self, id: int): + # TODO: implement + raise NotImplementedError + + async def create_message(self, *args, **kwargs): + # TODO: implement + raise NotImplementedError - @classmethod - def from_datetime(cls, dt: datetime) -> Snowflake: - return cls((int(dt.timestamp()) - DISCORD_EPOCH) << 22) + async def trigger_typing_indicator(self): + # TODO: implement + raise NotImplementedError diff --git a/pycord/types/snowflake.py b/pycord/object.py similarity index 80% rename from pycord/types/snowflake.py rename to pycord/object.py index 0d64dfdf..da6cb4d2 100644 --- a/pycord/types/snowflake.py +++ b/pycord/object.py @@ -19,14 +19,20 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations +from .mixins import Identifiable -from typing import Sequence, TypeVar, Union +__all__ = ( + "Object", +) -__all__: Sequence[str] = ('Snowflake', 'SnowflakeL', 'SnowflakeOr') -T = TypeVar('T', covariant=True) +class Object(Identifiable): + __slots__ = ( + "id", + ) -Snowflake = Union[int, str] -SnowflakeL = list[Snowflake] -SnowflakeOr = Union[T, Snowflake] + def __init__(self, id: int) -> None: + self.id = id + + def __repr__(self) -> str: + return f"" diff --git a/pycord/pages/__init__.py b/pycord/pages/__init__.py deleted file mode 100644 index af67d163..00000000 --- a/pycord/pages/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -pycord.ext.pager -~~~~~~~~~~~~~~~~ -The form of paginating through pages in Pycord. - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .errors import * -from .paginator import * diff --git a/pycord/pages/errors.py b/pycord/pages/errors.py deleted file mode 100644 index 63284d2b..00000000 --- a/pycord/pages/errors.py +++ /dev/null @@ -1,33 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Sequence - -from ..errors import PycordException - -__all__: Sequence[str] = ('PagerException', 'NoMorePages') - - -class PagerException(PycordException): - ... - - -class NoMorePages(PagerException): - ... diff --git a/pycord/pages/paginator.py b/pycord/pages/paginator.py deleted file mode 100644 index 0f97178e..00000000 --- a/pycord/pages/paginator.py +++ /dev/null @@ -1,160 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Any, Generic, Protocol, Sequence, TypeVar - -from .errors import NoMorePages, PagerException - -T = TypeVar('T', covariant=True) -P = TypeVar('P', bound='Page') - -__all__: Sequence[str] = ('Page', 'Paginator') - - -class Page(Protocol[T]): - """The class for all Page Types to subclass.""" - - value: T - - async def interact_forward(self, *args, **kwargs) -> None: - """Interactions to do when the paginator issues a forwarded statement""" - - async def interact_backward(self, *args, **kwargs) -> None: - """Interactions to do when the paginator issues a backward statement""" - - -class Paginator(Generic[P]): - """ - Class for paginating between pages. - - Parameters - ---------- - pages: list[:class:`.Page`] - Predefined pages - """ - - def __init__(self, pages: list[P] | None = None) -> None: - if pages is None: - self._pages = [] - else: - self._pages = pages - self._previous_page: tuple[int, Page] | None = None - - # not meant to be directly called like this - def __next__(self) -> P: - if self._previous_page is None: - try: - page = self._pages[0] - except IndexError: - raise PagerException('No pages in paginator') - - self._previous_page = (0, page) - - return page - - new = self._previous_page[0] + 1 - - try: - page = self._pages[new] - except IndexError: - raise NoMorePages('No more pages left in the paginator') - - self._previous_page = (new, page) - - return page - - async def forward(self, *args, **kwargs) -> P: - """ - Go forward through pages. - - Parameters - ---------- - args/kwargs: - Arguments and Keyword-Arguments to put into page.interact_forward. - """ - page = next(self) - - await page.interact_forward(*args, **kwargs) - return page.value - - async def backward(self, *args, **kwargs) -> P: - """ - Go backwards from the paginator. - Only works if the page is not the first page. - - Parameters - ---------- - args/kwargs: - Arguments and Keyword-Arguments to put into page.interact_backward. - """ - if self._previous_page is None or self._previous_page[0] == 0: - raise PagerException('Unable to go backwards without available pages') - - page = self._pages[self._previous_page[0] - 1] - self._previous_page = ( - None - if (self._previous_page[0] - 1) <= 0 - else (self._previous_page[0] - 1, self._pages[self._previous_page[0] - 1]) - ) - - await page.interact_backward(*args, **kwargs) - return page.value - - @property - def previous(self) -> P | None: - """ - The Previous page of this Paginator - """ - return None if self._previous_page is None else self._previous_page[1].value - - def add_page(self, page: P) -> None: - """ - Appends a new page to this Paginator - - Parameters - ---------- - page: :class:`.Page` - The page to append - """ - if page in self._pages: - raise PagerException('This page has already been added to this paginator') - self._pages.append(page) - - def remove_page(self, page: P) -> None: - """ - Removes a page from this paginator - - Parameters - ---------- - page: :class:`.Page` - The page to remove - """ - if page not in self._pages: - raise PagerException('This page is not part of this paginator') - self._pages.remove(page) - - async def __anext__(self) -> P: - try: - return await self.forward() - except NoMorePages: - raise StopAsyncIteration - - def __aiter__(self): - return self diff --git a/pycord/banner.txt b/pycord/panes/banner.txt similarity index 100% rename from pycord/banner.txt rename to pycord/panes/banner.txt diff --git a/pycord/ibanner.txt b/pycord/panes/informer.txt similarity index 74% rename from pycord/ibanner.txt rename to pycord/panes/informer.txt index 21e40fc4..b9d78b68 100644 --- a/pycord/ibanner.txt +++ b/pycord/panes/informer.txt @@ -1,4 +1,4 @@ ${bold_red}Wel${reset}${blue}come${reset} ${bold_blue}to${reset} ${yellow}Pycord v${version}${reset} - ${bold_white}It's currently ${current_time}, running on Python ${py_version}${reset} + ${bold_white}It's currently ${current_time}. Being run on Python ${py_version}${reset} ${bold_yellow}${botname}${reset} currently has ${red}${concurrency}${reset} connects left and will be running ${bold_blue}${shardcount}${reset} shard${sp}. diff --git a/pycord/role.py b/pycord/role.py deleted file mode 100644 index 81250ec8..00000000 --- a/pycord/role.py +++ /dev/null @@ -1,89 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .color import Color -from .flags import Permissions -from .snowflake import Snowflake - -if TYPE_CHECKING: - from .state import State - -from .missing import MISSING, Maybe, MissingEnum -from .types import Role as DiscordRole, RoleTags as DiscordRoleTags - - -class RoleTags: - __slots__ = ('bot_id', 'integration_id', 'premium_subscriber') - - def __init__(self, data: DiscordRoleTags) -> None: - self.bot_id: MissingEnum | Snowflake = ( - Snowflake(data.get('bot_id')) - if data.get('bot_id', MISSING) is not MISSING - else MISSING - ) - self.integration_id: MissingEnum | Snowflake = ( - Snowflake(data.get('integration_id')) - if data.get('integration_id', MISSING) is not MISSING - else MISSING - ) - self.premium_subscriber: MissingEnum | None = data.get( - 'premium_subscriber', MISSING - ) - - -class Role: - __slots__ = ( - '_state', - '_tags', - 'id', - 'name', - 'color', - 'hoist', - 'icon', - 'unicode_emoji', - 'position', - 'permissions', - 'managed', - 'mentionable', - 'tags', - ) - - def __init__(self, data: DiscordRole, state: State) -> None: - self._state = state - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['name'] - self.color: Color = Color(data['color']) - self.hoist: bool = data['hoist'] - self.icon: str | None | MissingEnum = data.get('icon', MISSING) - self.unicode_emoji: str | None | MissingEnum = data.get( - 'unicode_emoji', MISSING - ) - self.position: int = data['position'] - self.permissions: Permissions = Permissions.from_value(data['permissions']) - self.managed: bool = data['managed'] - self.mentionable: bool = data['mentionable'] - self._tags: dict[str, str | None] | MissingEnum = data.get('tags', MISSING) - self.tags: RoleTags | MissingEnum = ( - RoleTags(self._tags) if self._tags is not MISSING else MISSING - ) diff --git a/pycord/scheduled_event.py b/pycord/scheduled_event.py deleted file mode 100644 index 42615854..00000000 --- a/pycord/scheduled_event.py +++ /dev/null @@ -1,108 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING, Any - -from .enums import ( - GuildScheduledEventEntityType, - GuildScheduledEventPrivacyLevel, - GuildScheduledEventStatus, -) -from .member import Member -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import EntityMetadata as DiscordEntityMetadata, GuildScheduledEvent -from .user import User - -if TYPE_CHECKING: - from .state import State - - -class EntityMetadata: - def __init__(self, data: DiscordEntityMetadata) -> None: - self.location: MissingEnum | str = data.get('location', MISSING) - - -class ScheduledEventUser: - def __init__(self, data: dict[str, Any], state: State) -> None: - self.guild_scheduled_event_id: Snowflake = Snowflake( - data['guild_scheduled_event_id'] - ) - self.user: User = User(data['user'], state) - self.member: Member | MissingEnum = ( - Member(data['member'], state) if data.get('member') is not None else MISSING - ) - - -class ScheduledEvent: - def __init__(self, data: GuildScheduledEvent, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self._channel_id: str | None = data.get('channel_id') - self.channel_id: Snowflake | None = ( - Snowflake(self._channel_id) if self._channel_id is not None else None - ) - self._creator_id: MissingEnum | None | str = data.get('creator_id', MISSING) - self.creator_id: MissingEnum | None | Snowflake = ( - Snowflake(self._creator_id) - if isinstance(self._creator_id, str) - else self._creator_id - ) - self.name: str = data['name'] - self.description: MissingEnum | str | None = data.get('description', MISSING) - self.scheduled_start_time: datetime = datetime.fromisoformat( - data['scheduled_start_time'] - ) - self._scheduled_end_time: str | None = data.get('scheduled_end_time') - self.scheduled_end_time: datetime | None = ( - datetime.fromisoformat(self._scheduled_end_time) - if self._scheduled_end_time is not None - else None - ) - self.privacy_level: GuildScheduledEventPrivacyLevel = ( - GuildScheduledEventPrivacyLevel(data['privacy_level']) - ) - self.status: GuildScheduledEventStatus = GuildScheduledEventStatus( - data['status'] - ) - self.entity_type: GuildScheduledEventEntityType = GuildScheduledEventEntityType( - data['entity_type'] - ) - self._entity_id: str | None = data.get('entity_id') - self.entity_id: Snowflake | None = ( - Snowflake(self._entity_id) if self._entity_id is not None else None - ) - self._entity_metadata: dict[str, Any] | MissingEnum = data.get( - 'entity_metadata', MISSING - ) - self.entity_metadata: EntityMetadata | MissingEnum = ( - EntityMetadata(self._entity_metadata) - if self._entity_metadata is not MISSING - else MISSING - ) - self._creator: dict[str, Any] | MissingEnum = data.get('creator', MISSING) - self.creator: User | MissingEnum = ( - User(self._creator, state) if self._creator is not MISSING else MISSING - ) - self.user_count: int | MissingEnum = data.get('user_count', MISSING) - self._image: None | str | MissingEnum = data.get('image', MISSING) diff --git a/pycord/stage_instance.py b/pycord/stage_instance.py index 04aefe7a..b5a8d52c 100644 --- a/pycord/stage_instance.py +++ b/pycord/stage_instance.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,30 +18,58 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations -from typing import TYPE_CHECKING from .enums import StageInstancePrivacyLevel -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import StageInstance as DiscordStageInstance +from .mixins import Identifiable + +from typing import TYPE_CHECKING if TYPE_CHECKING: + from discord_typings import StageInstanceData + from .state import State -class StageInstance: - def __init__(self, data: DiscordStageInstance, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.guild_id: Snowflake = Snowflake(data['guild_id']) - self.channel_id: Snowflake = Snowflake(data['channel_id']) - self.topic: str = data['topic'] - self.privacy_level: StageInstancePrivacyLevel = StageInstancePrivacyLevel( - data['privacy_level'] - ) - self.guild_scheduled_event_id: MissingEnum | Snowflake = ( - Snowflake(data['guild_scheduled_event_id']) - if data.get('guild_scheduled_event_id') is not None - else MISSING - ) +class StageInstance(Identifiable): + __slots__ = ( + "id", + "guild_id", + "channel_id", + "topic", + "privacy_level", + "discoverable_disabled", + "guild_scheduled_event_id", + ) + + def __init__(self, *, data: "StageInstanceData", state: "State"): + self._state: "State" = state + self._update(data) + + def _update(self, data: "StageInstanceData"): + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + self.channel_id: int = int(data["channel_id"]) + self.topic: str = data["topic"] + self.privacy_level: StageInstancePrivacyLevel = StageInstancePrivacyLevel(data["privacy_level"]) + self.discoverable_disabled: bool = data["discoverable_disabled"] + self.guild_scheduled_event_id: int | None = int(gseid) if ( + gseid := data.get("guild_scheduled_event_id")) else None + + def __repr__(self) -> str: + return f"" + + async def modify( + self, + *, + topic: str = None, + privacy_level: StageInstancePrivacyLevel = None, + discoverable_disabled: bool = None, + reason: str | None = None, + ) -> "StageInstance": + # TODO: implement + raise NotImplementedError + + async def delete(self, *, reason: str | None = None) -> None: + # TODO: implement + raise NotImplementedError diff --git a/pycord/state/__init__.py b/pycord/state/__init__.py index 750aa59a..8a4cfe86 100644 --- a/pycord/state/__init__.py +++ b/pycord/state/__init__.py @@ -1,11 +1,11 @@ """ pycord.state ~~~~~~~~~~~~ -Pycord's State, keeps track of everything. +Modules used for Pycord's central bot state. -:copyright: 2021-present Pycord Development -:license: MIT +:copyright: 2021-present Pycord +:license: MIT, see LICENSE for more info. """ + +from .cache import * from .core import * -from .grouped_store import * -from .store import * diff --git a/pycord/state/cache.py b/pycord/state/cache.py new file mode 100644 index 00000000..2ccd7db2 --- /dev/null +++ b/pycord/state/cache.py @@ -0,0 +1,110 @@ +# MIT License +# +# Copyright (c) 2023 Pycord +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import gc +import logging +import weakref +from collections import OrderedDict +from typing import Any, Literal, Type, TypeVar, cast + +_log = logging.getLogger() + +T = TypeVar("T") + + +class Store: + """The default Pycord store.""" + + def __init__(self, max_items: int = 0) -> None: + self._store: OrderedDict[Any, Any] = OrderedDict() + self._weak_store: dict[Any, weakref.ReferenceType[Any]] = {} + self.max_items = max_items + gc.callbacks.append(self.__garbage_collector) + + async def get(self, key: Any, *, t: T) -> T | None: + ival = self._store.get(key) + + if ival: + return cast(T, ival) + + weak_val = self._weak_store.get(key) + + if weak_val is not None and weak_val() is not None: + return cast(T, weak_val()) + + return None + + async def upsert( + self, key: Any, object: Any, *, extra_keys: list[Any] | None = None + ) -> None: + self._store[key] = object + + if extra_keys: + ref = weakref.ref(object) + for key in extra_keys: + self._weak_store[key] = ref + + async def delete(self, key: Any) -> None: + del self._store[key] + + def __garbage_collector( + self, phase: Literal["start", "stop"], info: dict[str, int] + ) -> None: + """Collect all weakrefs which are no longer in use.""" + + del info + + if phase == "stop": + return + + _log.debug("cleaning up weak references") + + if self.max_items != 0 and len(self._store) > self.max_items: + for _ in range(len(self._store) - self.max_items): + self._store.popitem() + + for key, ref in self._weak_store.copy().items(): + if ref() is None: + _log.debug(f"removing {key} from weak store") + del self._weak_store[key] + + async def close(self) -> None: + gc.callbacks.remove(self.__garbage_collector) + + +class CacheStore: + def __init__(self, store_class: Type[Store]) -> None: + self._stores: dict[str, Store] = {} + self._store_class = store_class + + def __setitem__(self, key: str, val: int) -> None: + self._stores[key] = self._store_class(val) + + def __getitem__(self, key: str) -> Store: + istore = self._stores.get(key) + + if istore: + return istore + else: + store = self._store_class() + self._stores[key] = store + return store diff --git a/pycord/state/core.py b/pycord/state/core.py index 31c13a70..5ca071e5 100644 --- a/pycord/state/core.py +++ b/pycord/state/core.py @@ -1,5 +1,6 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# MIT License +# +# Copyright (c) 2023 Pycord # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -17,145 +18,57 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations +# SOFTWARE. -import asyncio -from typing import TYPE_CHECKING, Any, TypeVar +from typing import Any, Iterable, Type from aiohttp import BasicAuth -from ..api import HTTPClient -from ..commands.application import ApplicationCommand -from ..events import GuildCreate -from ..events.channels import ( - ChannelCreate, - ChannelDelete, - ChannelPinsUpdate, - ChannelUpdate, - MessageBulkDelete, - MessageCreate, - MessageDelete, - MessageUpdate, -) -from ..events.event_manager import EventManager -from ..events.guilds import ( - GuildBanCreate, - GuildBanDelete, - GuildDelete, - GuildMemberAdd, - GuildMemberChunk, - GuildMemberRemove, - GuildRoleCreate, - GuildRoleDelete, - GuildRoleUpdate, - GuildUpdate, -) -from ..events.other import InteractionCreate, Ready, UserUpdate -from ..flags import Intents -from ..missing import MISSING -from ..ui import Component -from ..ui.house import House -from ..ui.text_input import Modal -from ..user import User -from .grouped_store import GroupedStore - -T = TypeVar('T') - -BASE_EVENTS = [ - Ready, - GuildCreate, - GuildUpdate, - GuildDelete, - GuildBanCreate, - GuildBanDelete, - GuildMemberAdd, - GuildMemberRemove, - GuildMemberChunk, - GuildRoleCreate, - GuildRoleUpdate, - GuildRoleDelete, - ChannelCreate, - ChannelUpdate, - ChannelDelete, - ChannelPinsUpdate, - MessageCreate, - MessageUpdate, - MessageDelete, - MessageBulkDelete, - UserUpdate, - InteractionCreate, -] +from ..internal.event_manager import EventManager +from ..internal.gateway import Gateway +from ..internal.http import HTTPClient +from ..internal.reserver import Reserver +from .cache import CacheStore, Store -if TYPE_CHECKING: - from ..commands.command import Command - from ..ext.gears import Gear - from ..flags import Intents - from ..gateway import PassThrough, ShardCluster, ShardManager +BASE_MODELS: dict[Any, Any] = {} class State: - def __init__(self, **options: Any) -> None: - self.options = options - self.max_messages: int | None = options.get('max_messages', 1000) - self.large_threshold: int = options.get('large_threshold', 250) - self.shard_concurrency: PassThrough | None = None - self.intents: Intents = options.get('intents', Intents()) - self.user: User | None = None - self.raw_user: dict[str, Any] | None = None - self.store = GroupedStore(messages_max_items=self.max_messages) - self.event_manager = EventManager(BASE_EVENTS, self) - self.shard_managers: list[ShardManager] = [] - self.shard_clusters: list[ShardCluster] = [] - self.commands: list[Command] = [] - self.gears: list[Gear] = [] - self._session_start_limit: dict[str, Any] | None = None - self._clustered: bool | None = None - # makes sure that multiple clusters don't start at once - self._cluster_lock: asyncio.Lock = asyncio.Lock() - self._ready: bool = False - self.application_commands: list[ApplicationCommand] = [] - self.update_commands: bool = options.get('update_commands', True) - self.verbose: bool = options.get('verbose', False) - self.components: list[Component] = [] - self._component_custom_ids: list[str] = [] - self._components_via_custom_id: dict[str, Component] = {} - self.modals: list[Modal] = [] - self.cache_guild_members: bool = options.get('cache_guild_members', True) - - def sent_modal(self, modal: Modal) -> None: - if modal not in self.modals: - self.modals.append(modal) - - def sent_component(self, comp: Component) -> None: - if comp.id not in self._component_custom_ids and comp.id is not MISSING: - self.components.append(comp) - self._component_custom_ids.append(comp.id) - self._components_via_custom_id[comp.id] = comp - elif comp.disabled != self._components_via_custom_id[comp.id].disabled: - oldc = self._components_via_custom_id[comp.id] - self.components.remove(oldc) - self.components.append(comp) - self._components_via_custom_id[comp.id] = comp - - def sent_house(self, house: House) -> None: - for comp in house.components.values(): - self.sent_component(comp) + """The central bot cache.""" - def bot_init( + def __init__( self, token: str, - clustered: bool, + # cache-options + max_messages: int, + max_members: int, + intents: int, + gateway_large_threshold: int = 250, + # "advanced" options + base_url: str = "https://discord.com/api/v10", proxy: str | None = None, proxy_auth: BasicAuth | None = None, + shards: Iterable[int] | None = None, + # classes + store_class: Type[Store] = Store, + cache_model_classes: dict[Any, Any] = BASE_MODELS, ) -> None: - self.token = token - self.http = HTTPClient( - token=token, - base_url=self.options.get('http_base_url', 'https://discord.com/api/v10'), - proxy=proxy, - proxy_auth=proxy_auth, - verbose=self.verbose, + self.shards = shards or range(1) + self._token = token + self.cache = CacheStore(store_class) + self.cache["messages"] = max_messages + self.cache["members"] = max_members + self.max_members = max_members + self.intents = intents + self.http = HTTPClient(token, base_url, proxy, proxy_auth) + self.gateway = Gateway( + self, 10, None, None, shards or [0], len(shards) if shards else 1 ) - self._clustered = clustered + self.cache_models = cache_model_classes + # this is just a really low default. + # it should be set at the start, just in case however, + # this is here. + self.shard_rate_limit = Reserver(1, 5) + self.event_manager = EventManager() + self.large_threshold = gateway_large_threshold + self.user_id: int | None = None diff --git a/pycord/state/grouped_store.py b/pycord/state/grouped_store.py deleted file mode 100644 index 8032f9a3..00000000 --- a/pycord/state/grouped_store.py +++ /dev/null @@ -1,59 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -from .store import Store - - -class GroupedStore: - __slots__ = ('_stores', '_stores_dict', '_kwargs') - - def __init__(self, **max_items) -> None: - self._stores = [] - self._stores_dict = {} - self._kwargs = max_items - - def get_stores(self) -> list[Store]: - return self._stores - - def get_store(self, name: str) -> Store: - return self._stores[name] - - def discard(self, name: str) -> None: - d = self._stores_dict.get(name) - if d is not None: - self._stores_dict.pop(name) - self._stores.remove(d) - - def sift(self, name: str) -> Store: - s = self._stores_dict.get(name) - if s is not None: - return s - - for k, v in self._kwargs.items(): - if k == (name + '_max_items'): - store = Store(v) - else: - store = Store() - - self._stores.append(store) - self._stores_dict[name] = store - return store diff --git a/pycord/state/store.py b/pycord/state/store.py deleted file mode 100644 index 6723fd96..00000000 --- a/pycord/state/store.py +++ /dev/null @@ -1,110 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Any, Type, TypeVar - - -class _stored: - __slots__ = ('parents', 'id', 'storing') - - def __init__(self, parents: set[Any], self_id: Any, storing: Any) -> None: - self.parents = parents - self.id = self_id - self.storing = storing - - -T = TypeVar('T') - - -class Store: - __slots__ = ('_store', 'max_items') - - _store: set[_stored] - - def __init__(self, max_items: int | None = None) -> None: - self._store = set() - self.max_items = max_items - - async def get_one(self, parents: list[Any], id: Any) -> Any | None: - ps = set(parents) - - for store in self._store: - if store.parents & ps and store.id == id: - return store.storing - - async def get_without_parents(self, id: Any) -> tuple[set[Any], Any] | None: - for store in self._store: - if store.id == id: - return store.parents, store.storing - - async def insert(self, parents: list[Any], id: Any, data: Any) -> None: - if self.max_items and len(self._store) == self.max_items: - self._store = set() - - self._store.add(_stored(set(parents), id, data)) - - async def save(self, parents: list[Any], id: Any, data: Any) -> Any | None: - ps = set(parents) - - for store in self._store: - if store.parents & ps and store.id == id: - old_data = store.storing - self._store.discard(store) - store.storing = data - self._store.add(store) - return old_data - else: - if self.max_items and len(self._store) == self.max_items: - self._store = {} - - store = _stored(set(parents), id, data) - self._store.add(store) - - async def discard( - self, parents: list[Any], id: Any, type: Type[T] | T = Any - ) -> T | None: - ps = set(parents) - - for store in self._store: - if store.parents & ps or store.id == id: - self._store.remove(store) - return store - - async def get_all(self): - for store in self._store: - yield store.storing - - async def get_all_parent(self, parents: list[Any]): - ps = set(parents) - - for store in self._store: - if store.parents & ps: - yield store.storing - - async def delete_all(self) -> None: - self._store.clear() - - async def delete_all_parent(self, parents: list[Any]) -> None: - ps = set(parents) - - for store in self._store: - if store.parents & ps: - self._store.remove(store) diff --git a/pycord/sticker.py b/pycord/sticker.py new file mode 100644 index 00000000..6f2cdb86 --- /dev/null +++ b/pycord/sticker.py @@ -0,0 +1,146 @@ +# cython: language_level=3 +# Copyright (c) 2022-present Pycord Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE + +from .asset import Asset +from .user import User +from .enums import StickerFormatType, StickerType +from .missing import Maybe, MISSING +from .mixins import Identifiable + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from discord_typings import StickerData, StickerPackData + + from .state import State + + +class Sticker(Identifiable): + __slots__ = ( + "id", + "pack_id", + "name", + "description", + "tags", + "type", + "format_type", + "available", + "guild_id", + "user_id", + "sort_value", + ) + + def __init__(self, data: "StickerData", state: "State"): + self._state: "State" = state + self._update(data) + + def _update(self, data: "StickerData"): + self.id: int = int(data["id"]) + self.pack_id: Maybe[int] = int(pid) if (pid := data.get("pack_id")) else MISSING + self.name: str = data["name"] + self.description: str | None = data["description"] + self.tags: str | None = data["tags"] + self.type: StickerType = StickerType(data["type"]) + self.format_type: StickerFormatType = StickerFormatType(data["format_type"]) + self.available: Maybe[bool] = data.get("available", MISSING) + self.guild_id: Maybe[int] = int(gid) if (gid := data.get("guild_id")) else MISSING + self.user: Maybe[User] = User(data=user, state=self._state) if (user := data.get("user")) else MISSING + self.sort_value: Maybe[int] = int(sv) if (sv := data.get("sort_value")) else MISSING + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + async def modify( + self, + *, + name: str = None, + description: str = None, + tags: str = None, + reason: str | None = None, + ) -> "Sticker": + if self.guild_id is None: + raise Exception("Sticker has no guild attached") + # TODO: implement + raise NotImplementedError + + async def delete(self, *, reason: str | None = None) -> None: + if self.guild_id is None: + raise Exception("Sticker has no guild attached") + # TODO: implement + raise NotImplementedError + + @property + def asset(self) -> Asset: + formats = { + StickerFormatType.PNG: "png", + StickerFormatType.APNG: "apng", + StickerFormatType.LOTTIE: "json", + } + return Asset.from_sticker(self._state, self.id, formats.get(self.format_type, "png")) + + +class StickerPack(Identifiable): + __slots__ = ( + "id", + "stickers", + "name", + "sku_id", + "cover_sticker_id", + "description", + "banner_asset_id", + ) + + def __init__(self, *, data: "StickerPackData", state: "State"): + self._state: "State" = state + self._update(data) + + def _update(self, data: "StickerPackData"): + self.id: int = int(data["id"]) + self.stickers: list[Sticker] = [Sticker(data=sticker, state=self._state) for sticker in data["stickers"]] + self.name: str = data["name"] + self.sku_id: int = int(data["sku_id"]) + self.cover_sticker_id: Maybe[int] = int(coverid) if (coverid := data.get("cover_sticker_id")) else MISSING + self.description: str = data["description"] + self.banner_asset_id: Maybe[int] = int(bannerid) if (bannerid := data.get("banner_asset_id")) else MISSING + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + @property + def cover_sticker_asset(self) -> Asset | None: + if self.cover_sticker_id is None: + return None + sticker = next((sticker for sticker in self.stickers if sticker.id == self.cover_sticker_id), None) + if sticker is None: + return None + return sticker.asset + + @property + def banner_asset(self) -> Asset | None: + if self.banner_asset_id is None: + return None + return Asset.from_sticker_pack_banner(self._state, self.banner_asset_id, "png") diff --git a/pycord/gateway/notifier.py b/pycord/task_descheduler.py similarity index 54% rename from pycord/gateway/notifier.py rename to pycord/task_descheduler.py index 57ceed40..ff520e95 100644 --- a/pycord/gateway/notifier.py +++ b/pycord/task_descheduler.py @@ -1,4 +1,6 @@ +# -*- coding: utf-8 -*- # cython: language_level=3 +# Copyright (c) 2021-present VincentRPS # Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,34 +20,42 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations -import logging -from typing import TYPE_CHECKING -from .shard import Shard +import asyncio +import gc +from contextlib import asynccontextmanager +from typing import Any, AsyncGenerator, Literal -if TYPE_CHECKING: - from .manager import ShardManager -_log = logging.getLogger(__name__) +class TaskDescheduler: + def __init__(self) -> None: + self.active_tasks: list[asyncio.Future[Any]] = [] + gc.callbacks.append(self.__collect_finished_tasks) + def __getitem__(self, item: asyncio.Future[Any]) -> None: + self.active_tasks.append(item) -class Notifier: - def __init__(self, manager: ShardManager) -> None: - self.manager = manager + def __collect_finished_tasks( + self, phase: Literal["start", "stop"], info: dict[str, int] + ) -> None: + del info - async def shard_died(self, shard: Shard) -> None: - _log.debug(f'Shard {shard.id} died, restarting it') - shard_id = shard.id - self.manager.remove_shard(shard) - del shard + if phase == "stop": + return - new_shard = Shard( - id=shard_id, - state=self.manager._state, - session=self.manager.session, - notifier=self, - ) - await new_shard.connect(token=self.manager._state.token) - self.manager.add_shard(new_shard) + active_tasks = self.active_tasks.copy() + + for task in active_tasks: + if task.done(): + self.active_tasks.remove(task) + + del active_tasks + + +Tasks = TaskDescheduler() + + +@asynccontextmanager +async def tasks() -> AsyncGenerator[TaskDescheduler, Any]: + yield Tasks diff --git a/pycord/team.py b/pycord/team.py deleted file mode 100644 index 09f6d939..00000000 --- a/pycord/team.py +++ /dev/null @@ -1,43 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from .enums import MembershipState -from .snowflake import Snowflake -from .types import Team as DiscordTeam, TeamMember as DiscordTeamMember -from .user import User - - -class TeamMember: - def __init__(self, data: DiscordTeamMember) -> None: - self.team_id: Snowflake = Snowflake(data['team_id']) - self.user = User(data['user']) - self.permissions: list[str] = data['permissions'] - self.membership_state: MembershipState = MembershipState( - data['membership_state'] - ) - - -class Team: - def __init__(self, data: DiscordTeam) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.icon: str | None = data['icon'] - self.members: list[TeamMember] = [TeamMember(d) for d in data['members']] - self.name: str = data['name'] - self.owner_id: Snowflake = Snowflake(data['owner_user_id']) diff --git a/pycord/traits/__init__.py b/pycord/traits/__init__.py new file mode 100644 index 00000000..bc80a6ae --- /dev/null +++ b/pycord/traits/__init__.py @@ -0,0 +1,8 @@ +""" +pycord.traits +~~~~~~~~~~~~~ +Traits for reimplementing certain classes in Pycord. + +:copyright: 2021-present Pycord +:license: MIT +""" diff --git a/pycord/types/__init__.py b/pycord/types/__init__.py deleted file mode 100644 index bad91ce8..00000000 --- a/pycord/types/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -pycord.types -~~~~~~~~~~~~ -Typing's for the Discord API. - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from typing import Any, Callable, Coroutine - -from .application import * -from .application_commands import * -from .application_role_connection_metadata import * -from .audit_log import * -from .auto_moderation import * -from .channel import * -from .component import * -from .embed import * -from .guild import * -from .guild_scheduled_event import * -from .guild_template import * -from .integration import * -from .interaction import * -from .invite import * -from .media import * -from .message import * -from .role import * -from .snowflake import * -from .stage_instance import * -from .user import * -from .voice_state import * -from .webhook import * -from .welcome_screen import * - -AsyncFunc = Callable[..., Coroutine[Any, Any, Any]] diff --git a/pycord/types/application.py b/pycord/types/application.py deleted file mode 100644 index 7ef9e097..00000000 --- a/pycord/types/application.py +++ /dev/null @@ -1,73 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .integration import SCOPE -from .snowflake import Snowflake -from .user import User - - -class TeamMember(TypedDict): - membership_state: Literal[1, 2] - permissions: list[str] - team_id: Snowflake - user: User - - -class Team(TypedDict): - icon: str | None - id: Snowflake - members: list[TeamMember] - name: str - owner_user_id: Snowflake - - -class InstallParams(TypedDict): - scopes: list[SCOPE] - permissions: str - - -class Application(TypedDict): - id: Snowflake - name: str - icon: str | None - description: str - rpc_origins: NotRequired[list[str]] - bot_public: bool - bot_require_code_grant: bool - terms_of_service_url: NotRequired[str] - privacy_policy_url: NotRequired[str] - owner: NotRequired[User] - summary: NotRequired[str] - verify_key: str - team: Team | None - guild_id: NotRequired[Snowflake] - primary_sku_id: NotRequired[Snowflake] - slug: NotRequired[str] - cover_image: NotRequired[str] - flags: NotRequired[int] - tags: NotRequired[list[str]] - install_params: InstallParams - custom_install_url: NotRequired[str] - role_connections_verification_url: NotRequired[str] diff --git a/pycord/types/application_commands.py b/pycord/types/application_commands.py deleted file mode 100644 index df490f29..00000000 --- a/pycord/types/application_commands.py +++ /dev/null @@ -1,99 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .channel import CTYPE -from .snowflake import Snowflake -from .user import LOCALE - -ATYPE = Literal[ - 1, - 2, - 3, -] -AOTYPE = Literal[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, -] - - -class ApplicationCommandOptionChoice(TypedDict): - name: str - name_localizations: NotRequired[dict[LOCALE] | None] - value: str | int | float - - -class ApplicationCommandOption(TypedDict): - type: ATYPE - name: str - name_localizations: NotRequired[dict[LOCALE, str] | None] - description: str - description_localizations: NotRequired[dict[LOCALE, str] | None] - required: NotRequired[bool] - choices: NotRequired[ApplicationCommandOptionChoice] - options: NotRequired[list['ApplicationCommandOption']] - channel_types: NotRequired[list[CTYPE]] - min_value: NotRequired[int] - max_value: NotRequired[int] - min_length: NotRequired[int] - max_length: NotRequired[int] - autocomplete: NotRequired[bool] - - -class ApplicationCommand(TypedDict): - id: Snowflake - type: NotRequired[ATYPE] - application_id: Snowflake - guild_id: NotRequired[Snowflake] - name: str - name_localizations: NotRequired[dict[LOCALE, str] | None] - description: str - description_localizations: NotRequired[dict[LOCALE, str] | None] - options: NotRequired[list[ApplicationCommandOption]] - default_member_permissions: str | None - dm_permission: NotRequired[bool] - default_permission: NotRequired[bool | None] - version: Snowflake - - -class ApplicationCommandPermissions(TypedDict): - id: Snowflake - type: Literal[1, 2, 3] - permission: bool - - -class GuildApplicationCommandPermissions(TypedDict): - id: Snowflake - application_id: Snowflake - guild_id: Snowflake - permissions: list[ApplicationCommandPermissions] diff --git a/pycord/types/application_role_connection_metadata.py b/pycord/types/application_role_connection_metadata.py deleted file mode 100644 index 3fd2682f..00000000 --- a/pycord/types/application_role_connection_metadata.py +++ /dev/null @@ -1,46 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .user import LOCALE - -RCMTYPE = Literal[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, -] - - -class ApplicationRoleConnectionMetadata(TypedDict): - type: RCMTYPE - key: str - name: str - name_localizations: NotRequired[dict[LOCALE, str]] - description: str - description_localizations: NotRequired[dict[LOCALE, str]] diff --git a/pycord/types/audit_log.py b/pycord/types/audit_log.py deleted file mode 100644 index 47ca59e7..00000000 --- a/pycord/types/audit_log.py +++ /dev/null @@ -1,131 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Any, Literal - -from typing_extensions import NotRequired, TypedDict - -from .application_commands import ApplicationCommand -from .auto_moderation import AutoModerationRule -from .channel import Channel -from .guild_scheduled_event import GuildScheduledEvent -from .integration import Integration -from .snowflake import Snowflake -from .user import User -from .webhook import Webhook - -AUDIT_LOG_EVENT_TYPE = Literal[ - 1, - 10, - 11, - 12, - 13, - 14, - 15, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 30, - 31, - 32, - 40, - 41, - 42, - 50, - 51, - 52, - 60, - 61, - 62, - 72, - 73, - 74, - 75, - 80, - 81, - 82, - 83, - 84, - 85, - 90, - 91, - 92, - 100, - 101, - 102, - 110, - 111, - 112, - 121, - 140, - 141, - 142, - 143, - 144, - 145, -] - - -class AuditLogChange(TypedDict): - new_value: NotRequired[Any] - old_value: NotRequired[Any] - key: str - - -class OptionalAuditEntryInfo(TypedDict): - application_id: Snowflake - auto_moderation_rule_name: str - auto_moderation_rule_trigger_type: str - channel_id: Snowflake - count: str - delete_member_days: str - id: Snowflake - members_removed: str - message_id: Snowflake - role_name: str - type: str - - -class AuditLogEntry(TypedDict): - target_id: str | None - changes: NotRequired[AuditLogChange] - user_id: Snowflake | None - id: Snowflake - action_type: AUDIT_LOG_EVENT_TYPE - options: NotRequired[OptionalAuditEntryInfo] - reason: NotRequired[str] - - -class AuditLog(TypedDict): - application_commands: list[ApplicationCommand] - audit_log_entries: list[AuditLogEntry] - auto_moderation_rules: list[AutoModerationRule] - guild_scheduled_events: list[GuildScheduledEvent] - integrations: list[Integration] - threads: list[Channel] - users: list[User] - webhooks: list[Webhook] diff --git a/pycord/types/auto_moderation.py b/pycord/types/auto_moderation.py deleted file mode 100644 index 53108968..00000000 --- a/pycord/types/auto_moderation.py +++ /dev/null @@ -1,63 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import TypedDict - -from .snowflake import Snowflake - -AUTO_MODERATION_TRIGGER_TYPES = Literal[1, 3, 4, 5] -AUTO_MODERATION_KEYWORD_PRESET_TYPES = Literal[1, 2, 3] -AUTO_MODERATION_EVENT_TYPES = Literal[1] -AUTO_MODERATION_ACTION_TYPES = Literal[1, 2, 3] - - -class AutoModerationActionMetadata(TypedDict): - channel_id: Snowflake - duration_seconds: int - - -class AutoModerationAction(TypedDict): - type: AUTO_MODERATION_ACTION_TYPES - metadata: AutoModerationActionMetadata - - -class AutoModerationTriggerMetadata(TypedDict): - keyword_filter: list[str] - regex_patterns: list[str] - presets: list[AUTO_MODERATION_KEYWORD_PRESET_TYPES] - allow_list: list[str] - mention_total_limit: int - - -class AutoModerationRule(TypedDict): - id: Snowflake - guild_id: Snowflake - name: str - creator_id: Snowflake - event_type: AUTO_MODERATION_EVENT_TYPES - trigger_type: AUTO_MODERATION_TRIGGER_TYPES - trigger_metadata: AutoModerationTriggerMetadata - actions: list[AutoModerationAction] - enabled: bool - exempt_roles: list[Snowflake] - exempt_channels: list[Snowflake] diff --git a/pycord/types/channel.py b/pycord/types/channel.py index 6017fcc3..e74e04ce 100644 --- a/pycord/types/channel.py +++ b/pycord/types/channel.py @@ -1,125 +1,20 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import TYPE_CHECKING, Literal +from typing import Any, Literal, NotRequired, Required, TypedDict -from typing_extensions import NotRequired, TypedDict +from discord_typings._resources._channel import PartialAttachmentData -from .snowflake import Snowflake -from .user import User -if TYPE_CHECKING: - from .guild import GuildMember - -CTYPE = Literal[0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15] - - -class Overwrite(TypedDict): - id: Snowflake - type: int - allow: str - deny: str - - -class ThreadMetadata(TypedDict): - archived: bool - auto_archive_duration: int - archive_timestamp: str - locked: bool - invitable: NotRequired[bool] - create_timestamp: NotRequired[str | None] - - -class ThreadMember(TypedDict): - id: NotRequired[Snowflake] - user_id: NotRequired[Snowflake] - join_timestamp: str +class ForumThreadMessageParams(TypedDict, total=False): + content: str + embeds: list[dict[str, Any]] + allowed_mentions: dict[str, Any] + components: list[dict[str, Any]] + sticker_ids: list[int] + attachments: list[PartialAttachmentData] flags: int - member: NotRequired['GuildMember'] - - -class ForumTag(TypedDict): - id: Snowflake - name: str - moderated: bool - emoji_id: Snowflake - emoji_name: str | None - - -class DefaultReaction(TypedDict): - emoji_id: str | None - emoji_name: str | None - - -class Channel(TypedDict): - id: Snowflake - type: CTYPE - guild_id: NotRequired[Snowflake] - position: NotRequired[Snowflake] - permission_overwrites: NotRequired[list[Overwrite]] - name: NotRequired[str | None] - topic: NotRequired[str | None] - nsfw: NotRequired[bool] - last_message_id: NotRequired[Snowflake | None] - bitrate: NotRequired[int] - user_limit: NotRequired[int] - rate_limit_per_user: NotRequired[int] - recipients: NotRequired[list[User]] - icon: NotRequired[str | None] - owner_id: NotRequired[Snowflake] - application_id: NotRequired[Snowflake] - parent_id: NotRequired[Snowflake | None] - last_pin_timestamp: NotRequired[str | None] - rtc_region: NotRequired[str | None] - video_quality_mode: NotRequired[Literal[1, 2]] - message_count: NotRequired[int] - member_count: NotRequired[int] - thread_metadata: NotRequired[ThreadMetadata] - member: NotRequired[ThreadMember] - default_auto_archive_duration: NotRequired[int] - permissions: NotRequired[int] - flags: NotRequired[int] - total_messages_sent: NotRequired[int] - available_tags: list[ForumTag] - applied_tags: list[Snowflake] - default_reaction_emoji: NotRequired[DefaultReaction] - default_thread_rate_limit_per_user: NotRequired[int] - default_sort_order: NotRequired[int | None] - - -AMTYPE = Literal['roles', 'users', 'everyone'] - - -class AllowedMentions(TypedDict): - parse: list[AMTYPE] - roles: list[Snowflake] - users: list[Snowflake] - replied_user: bool - - -class FollowedChannel(TypedDict): - channel_id: Snowflake - webhook_id: Snowflake -class ListThreadsResponse(TypedDict): - threads: list[Channel] - members: list[ThreadMember] - has_more: bool +class ChannelPositionUpdateData(TypedDict, total=False): + id: Required[int] + position: int | None + lock_permissions: bool | None + parent_id: int | None diff --git a/pycord/types/component.py b/pycord/types/component.py deleted file mode 100644 index 433806d8..00000000 --- a/pycord/types/component.py +++ /dev/null @@ -1,80 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .channel import CTYPE -from .media import Emoji - -COTYPE = Literal[1, 2, 3, 4, 5, 6, 7, 8] -BSTYLE = Literal[1, 2, 3, 4, 5] - - -class ActionRow(TypedDict): - components: list[Button | SelectMenu | TextInput] - - -class Button(TypedDict): - type: Literal[2] - style: BSTYLE - label: NotRequired[str] - emoji: NotRequired[Emoji] - custom_id: NotRequired[str] - url: NotRequired[str] - disabled: NotRequired[bool] - - -class SelectOption(TypedDict): - label: str - value: str - description: NotRequired[str] - emoji: NotRequired[Emoji] - default: NotRequired[bool] - - -class SelectMenu(TypedDict): - type: Literal[3, 5, 6, 7, 8] - custom_id: str - options: NotRequired[list[SelectOption]] - channel_types: list[CTYPE] - placeholder: NotRequired[str] - min_values: NotRequired[int] - mazx_values: NotRequired[int] - disabled: NotRequired[bool] - - -class TextInput(TypedDict): - type: Literal[4] - custom_id: str - style: Literal[1, 2] - label: str - min_length: NotRequired[int] - max_length: NotRequired[int] - required: NotRequired[bool] - value: NotRequired[str] - placeholder: NotRequired[str] - - -Component = ActionRow | SelectMenu | TextInput | Button diff --git a/pycord/types/embed.py b/pycord/types/embed.py deleted file mode 100644 index 8f4b0eec..00000000 --- a/pycord/types/embed.py +++ /dev/null @@ -1,86 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -ETYPES = Literal['rich', 'image', 'video', 'gifv', 'article', 'link'] - - -class Thumbnail(TypedDict): - url: str - proxy_url: NotRequired[str] - height: NotRequired[int] - width: NotRequired[int] - - -class Video(TypedDict): - url: str - proxy_url: NotRequired[str] - height: NotRequired[int] - width: NotRequired[int] - - -class Image(TypedDict): - url: str - proxy_url: NotRequired[str] - height: NotRequired[int] - width: NotRequired[int] - - -class Author(TypedDict): - name: str - url: NotRequired[str] - icon_url: NotRequired[str] - proxy_icon_url: NotRequired[str] - - -class Provider(TypedDict): - name: NotRequired[str] - url: NotRequired[str] - - -class Footer(TypedDict): - text: str - icon_url: NotRequired[str] - proxy_icon_url: NotRequired[str] - - -class Field(TypedDict): - name: str - value: str - inline: NotRequired[bool] - - -class Embed(TypedDict): - title: NotRequired[str] - type: NotRequired[ETYPES] - description: NotRequired[str] - url: NotRequired[str] - timestamp: NotRequired[str] - color: NotRequired[int] - footer: NotRequired[Footer] - image: NotRequired[Image] - thumbnail: NotRequired[Thumbnail] - video: NotRequired[Video] - provider: NotRequired[Provider] - author: NotRequired[Author] - fields: NotRequired[list[Field]] diff --git a/pycord/types/guild.py b/pycord/types/guild.py index aeaf63f7..4ea17a25 100644 --- a/pycord/types/guild.py +++ b/pycord/types/guild.py @@ -1,175 +1,19 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal +from typing import Literal, NotRequired, TypedDict -from typing_extensions import NotRequired, TypedDict -from .channel import Channel -from .media import Emoji, Sticker -from .role import Role -from .snowflake import Snowflake -from .user import LOCALE, User -from .welcome_screen import WelcomeScreen - -VERIFICATION_LEVEL = Literal[0, 1, 2, 3, 4] -DMNLEVEL = Literal[0, 1] -EC_FILTER = Literal[0, 1, 2] -GUILD_FEATURE = Literal[ - 'ANIMATED_BANNER', - 'ANIMATED_ICON', - 'AUTO_MODERATION', - 'COMMUNITY', - 'DEVELOPER_SUPPORT_SERVER', - 'DISCOVERABLE', - 'FEATURABLE', - 'INVITES_DISABLED', - 'INVITE_SPLASH', - 'MEMBER_VERIFICATION_GATE_ENABLED', - 'MONETIZATION_ENABLED', - 'MORE_STICKERS', - 'NEWS', - 'PARTNERED', - 'PREVIEW_ENABLED', - 'PRIVATE_THREADS', - 'ROLE_ICONS', - 'TICKETED_EVENTS_ENABLED', - 'VANITY_URL', - 'VERIFIED', - 'VIP_REGIONS', - 'WELCOME_SCREEN_ENABLED', -] -MFA_LEVEL = Literal[0, 1] -PREMIUM_TIER = Literal[0, 1, 2, 3] -NSFW_LEVEL = Literal[0, 1, 2, 3] -WIDGET_STYLE = Literal[ - 'shield', - 'banner1', - 'banner2', - 'banner3', - 'banner4', -] - - -class UnavailableGuild(TypedDict): - id: Snowflake - unavailable: Literal[True] - - -class GuildPreview(TypedDict): - id: Snowflake - name: str - icon: str | None - splash: str | None - discovery_splash: str | None - emojis: list[Emoji] - features: list[GUILD_FEATURE] - approximate_member_count: int - approximate_presence_count: int - description: str - stickers: list[Sticker] - - -class Guild(TypedDict): - id: Snowflake - name: str - icon: str | None - icon_hash: NotRequired[str | None] - splash: str | None - discovery_splash: str | None - owner: NotRequired[bool] - owner_id: Snowflake - permissions: NotRequired[str] - afk_channel_id: Snowflake | None - afk_timeout: int - widget_enabled: NotRequired[bool] - widget_channel_id: NotRequired[Snowflake | None] - verification_level: VERIFICATION_LEVEL - default_message_notifications: DMNLEVEL - explicit_content_filter: EC_FILTER - roles: list[Role] - emojis: list[Emoji] - features: list[GUILD_FEATURE] - mfa_level: MFA_LEVEL - application_id: str | None - system_channel_id: Snowflake | None - system_channel_flags: int - rules_channel_id: Snowflake | None - max_presences: NotRequired[int | None] - max_members: NotRequired[int] - vanity_url_code: str | None - description: str | None - banner: str | None - premium_tier: PREMIUM_TIER - premium_subscription_count: NotRequired[int] - preferred_locale: LOCALE - public_updates_channel_id: Snowflake | None - max_video_channel_users: NotRequired[int] - approximate_member_count: NotRequired[int] - approximate_presence_count: NotRequired[int] - welcome_screen: NotRequired[WelcomeScreen] - nsfw_level: NSFW_LEVEL - stickers: NotRequired[list[Sticker]] - premium_progress_bar_enabled: bool - - -class WidgetSettings(TypedDict): - enabled: bool - channel_id: Snowflake | None - - -class Widget(TypedDict): - id: Snowflake - name: str - instant_invite: str | None - channels: list[Channel] - members: list[User] - presence_count: int - - -class GuildMember(TypedDict): - user: NotRequired[User] - nick: NotRequired[str | None] - avatar: NotRequired[str | None] - roles: list[Snowflake] - joined_at: str - premium_since: NotRequired[str | None] - deaf: bool - mute: bool - pending: NotRequired[bool] - permissions: NotRequired[str] - communication_disabled_until: NotRequired[str | None] +class RolePositionUpdateData(TypedDict): + id: int + position: NotRequired[int | None] -class Ban(TypedDict): - reason: str | None - user: User +class MFALevelResponse(TypedDict): + mfa_level: Literal[0, 1] -class ModifyGuildChannelPositionsPayload(TypedDict): - id: Snowflake - position: int | None - lock_permissions: bool | None - parent_id: Snowflake | None +class PruneCountResponse(TypedDict): + pruned: int -class ModifyGuildRolePositionsPayload(TypedDict): - id: Snowflake - position: NotRequired[int | None] +class VanityURLData(TypedDict): + code: str | None + uses: int diff --git a/pycord/types/guild_scheduled_event.py b/pycord/types/guild_scheduled_event.py deleted file mode 100644 index 2dfa79f6..00000000 --- a/pycord/types/guild_scheduled_event.py +++ /dev/null @@ -1,56 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .guild import GuildMember -from .snowflake import Snowflake -from .user import User - - -class EntityMetadata(TypedDict): - location: str | None - - -class EventUser(TypedDict): - guild_scheduled_event_id: Snowflake - user: User - member: GuildMember - - -class GuildScheduledEvent(TypedDict): - id: Snowflake - guild_id: Snowflake - channel_id: Snowflake | None - creator_id: NotRequired[Snowflake] - name: str - description: NotRequired[str | None] - scheduled_start_time: str - scheduled_end_time: str | None - privacy_level: Literal[2] - status: Literal[1, 2, 3, 4] - entity_type: Literal[1, 2, 3] - entity_id: Snowflake | None - entity_metadata: EntityMetadata | None - creator: NotRequired[User] - user_count: NotRequired[int] - image: NotRequired[str | None] diff --git a/pycord/types/guild_template.py b/pycord/types/guild_template.py deleted file mode 100644 index a4dd3c4f..00000000 --- a/pycord/types/guild_template.py +++ /dev/null @@ -1,39 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing_extensions import TypedDict - -from .guild import Guild -from .snowflake import Snowflake -from .user import User - - -class GuildTemplate(TypedDict): - code: str - name: str - description: str | None - usage_count: int - creator_id: Snowflake - creator: User - created_at: str - updated_at: str - source_guild_id: Snowflake - serialized_source_guild: Guild - is_dirty: bool | None diff --git a/pycord/types/integration.py b/pycord/types/integration.py deleted file mode 100644 index 534505db..00000000 --- a/pycord/types/integration.py +++ /dev/null @@ -1,90 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .snowflake import Snowflake -from .user import User - -SCOPE = Literal[ - 'activities.read', - 'activities.write', - 'applications.builds.read', - 'applications.builds.upload', - 'applications.commands', - 'applications.commands.update', - 'applications.commands.permissions.update', - 'applications.entitlements', - 'applications.store.update', - 'bot', - 'connections', - 'dm_channels.read', - 'email', - 'guilds', - 'guilds.join', - 'guilds.members.read', - 'identify', - 'messages.send', - 'relationships.read', - 'rpc', - 'rpc.activities.write', - 'rpc.notifications.read', - 'rpc.voice.read', - 'rpc.voice.write', - 'voice', - 'webhook.incoming', -] -INTEGRATION_TYPE = Literal['twitch', 'youtube', 'discord'] -INTEGRATION_EXPIRE_BEHAVIOR = Literal[0, 1] - - -class Account(TypedDict): - id: str - name: str - - -class IntegrationApplication(TypedDict): - id: Snowflake - name: str - icon: str | None - description: str - bot: NotRequired[User] - - -class Integration(TypedDict): - id: Snowflake - name: str - type: INTEGRATION_TYPE - enabled: NotRequired[bool] - syncing: NotRequired[bool] - role_id: NotRequired[Snowflake] - enable_emoticons: NotRequired[bool] - expire_behavior: NotRequired[INTEGRATION_EXPIRE_BEHAVIOR] - expire_grace_period: NotRequired[int] - user: NotRequired[User] - account: Account - synced_at: NotRequired[str] - subscriber_count: NotRequired[int] - revoked: NotRequired[bool] - application: NotRequired[IntegrationApplication] - scopes: list[SCOPE] diff --git a/pycord/types/interaction.py b/pycord/types/interaction.py deleted file mode 100644 index 2615e61e..00000000 --- a/pycord/types/interaction.py +++ /dev/null @@ -1,135 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .application_commands import ATYPE, ApplicationCommandOptionChoice -from .channel import AllowedMentions, Channel -from .component import Component, SelectOption -from .embed import Embed -from .guild import GuildMember -from .media import Attachment -from .message import Message -from .role import Role -from .snowflake import Snowflake -from .user import LOCALE, User - -ITYPE = Literal[ - 1, - 2, - 3, - 4, - 5, -] - - -class ResolvedData(TypedDict): - users: NotRequired[list[User]] - members: NotRequired[list[GuildMember]] - roles: NotRequired[list[Role]] - channels: NotRequired[list[Channel]] - messages: NotRequired[list[Message]] - attachments: NotRequired[list[Attachment]] - - -class ApplicationCommandInteractionDataOption(TypedDict): - name: str - type: ATYPE - value: NotRequired[str | int | float] - options: NotRequired[list['ApplicationCommandInteractionDataOption']] - focused: NotRequired[bool] - - -class ApplicationCommandData(TypedDict): - id: Snowflake - name: str - type: ATYPE - resolved: NotRequired[ResolvedData] - options: NotRequired[list[ApplicationCommandInteractionDataOption]] - guild_id: NotRequired[Snowflake] - target_id: NotRequired[Snowflake] - - -class MessageComponentData(TypedDict): - custom_id: str - component_type: int - values: NotRequired[list[SelectOption]] - - -class ModalSubmitData(TypedDict): - custom_id: str - components: list[Component] - - -INTERACTION_DATA = ( - ApplicationCommandData - | ApplicationCommandInteractionDataOption - | ResolvedData - | ApplicationCommandData - | MessageComponentData - | ModalSubmitData -) - - -class Interaction(TypedDict): - id: Snowflake - application_id: Snowflake - type: ITYPE - data: INTERACTION_DATA - guild_id: NotRequired[Snowflake] - channel_id: NotRequired[Snowflake] - member: NotRequired[GuildMember] - user: NotRequired[User] - token: str - version: int - message: NotRequired[Message] - app_permissions: str - locale: LOCALE - guild_locale: LOCALE - - -class ICDMessages(TypedDict): - tts: NotRequired[bool] - content: NotRequired[str] - embeds: NotRequired[list[Embed]] - allowed_mentions: NotRequired[AllowedMentions] - flags: NotRequired[int] - components: NotRequired[list[Component]] - attachments: NotRequired[list[Attachment]] - - -class ICDAutocomplete(TypedDict): - choices: list[ApplicationCommandOptionChoice] - - -class ICDModal(TypedDict): - custom_id: str - title: str - components: list[Component] - - -ICD = ICDMessages | ICDAutocomplete | ICDModal - - -class InteractionResponse(TypedDict): - type: Literal[1, 4, 5, 6, 7, 8, 9] - data: ICD diff --git a/pycord/types/invite.py b/pycord/types/invite.py deleted file mode 100644 index e823c31d..00000000 --- a/pycord/types/invite.py +++ /dev/null @@ -1,54 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing_extensions import NotRequired, TypedDict - -from .application import Application -from .channel import Channel -from .guild import Guild -from .guild_scheduled_event import GuildScheduledEvent -from .user import User - - -class InviteMetadata(TypedDict): - uses: int - max_uses: int - max_age: int - temporary: bool - created_at: str - - -class Invite(TypedDict): - code: str - guild: NotRequired[Guild] - channel: Channel | None - inviter: NotRequired[User] - target_type: NotRequired[int] - target_user: NotRequired[User] - target_application: NotRequired[Application] - approximate_presence_count: NotRequired[int] - approximate_member_count: NotRequired[int] - expires_at: NotRequired[str] - guild_scheduled_event: NotRequired[GuildScheduledEvent] - - -class PartialInvite(TypedDict): - code: str | None - uses: int diff --git a/pycord/types/media.py b/pycord/types/media.py deleted file mode 100644 index f886d1e9..00000000 --- a/pycord/types/media.py +++ /dev/null @@ -1,78 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .snowflake import Snowflake -from .user import User - - -class Emoji(TypedDict): - id: Snowflake | None - name: str - roles: NotRequired[list[Snowflake]] - user: NotRequired[User] - require_colons: NotRequired[bool] - managed: NotRequired[bool] - animated: NotRequired[bool] - available: NotRequired[bool] - - -class StickerItem(TypedDict): - id: Snowflake - name: str - format_type: Literal[1, 2, 3] - - -class Sticker(StickerItem): - pack_id: NotRequired[Snowflake] - description: str | None - tags: str - asset: NotRequired[str] - type: Literal[1, 2] - available: NotRequired[bool] - guild_id: NotRequired[Snowflake] - user: NotRequired[User] - sort_value: NotRequired[int] - - -class StickerPack(TypedDict): - id: Snowflake - stickers: list[Sticker] - name: str - sku_id: Snowflake - cover_sticker_id: NotRequired[Snowflake] - description: str - banner_asset_id: NotRequired[Snowflake] - - -class Attachment(TypedDict): - id: Snowflake - filename: str - description: NotRequired[str] - content_type: NotRequired[str] - size: int - url: str - proxy_url: str - height: NotRequired[int | None] - width: NotRequired[int | None] - ephemeral: NotRequired[bool] diff --git a/pycord/types/message.py b/pycord/types/message.py deleted file mode 100644 index ab4af1c0..00000000 --- a/pycord/types/message.py +++ /dev/null @@ -1,138 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .application import Application -from .channel import CTYPE, AllowedMentions, Channel -from .component import Component -from .embed import Embed -from .guild import GuildMember -from .media import Attachment, Emoji, Sticker, StickerItem -from .role import Role -from .snowflake import Snowflake -from .user import User - -MTYPE = Literal[ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, -] - - -class ChannelMention(TypedDict): - id: Snowflake - guild_id: Snowflake - type: CTYPE - name: str - - -class Reaction(TypedDict): - count: int - me: bool - emoji: Emoji - - -class MessageActivity(TypedDict): - type: Literal[1, 2, 3, 5] - party_id: str - - -class MessageReference(TypedDict): - message_id: NotRequired[Snowflake] - channel_id: NotRequired[Snowflake] - guild_id: NotRequired[Snowflake] - fail_if_not_exists: NotRequired[bool] - - -class MessageInteraction(TypedDict): - id: Snowflake - type: Literal[1, 2, 3, 4, 5] - name: str - user: User - member: NotRequired[GuildMember] - - -class Message(TypedDict): - id: Snowflake - channel_id: Snowflake - author: NotRequired[User] - content: NotRequired[str] - timestamp: str - edited_timestamp: str | None - tts: bool - mention_everyone: bool - mentions: list[User] - mention_roles: list[Role] - mention_channels: NotRequired[list[ChannelMention]] - attachments: list[Attachment] - embeds: list[Embed] - reactions: list[Reaction] - nonce: NotRequired[int | str] - pinned: bool - webhook_id: Snowflake - type: int - activity: NotRequired[MessageActivity] - application: NotRequired[Application] - application_id: NotRequired[Snowflake] - message_reference: NotRequired[MessageReference] - flags: NotRequired[int] - referenced_message: NotRequired['Message'] - interaction: NotRequired[MessageInteraction] - thread: NotRequired[Channel] - components: NotRequired[list[Component]] - sticker_items: NotRequired[list[StickerItem]] - stickers: NotRequired[list[Sticker]] - position: NotRequired[int] - - -class ForumThreadMessageParams(TypedDict): - content: NotRequired[str] - embeds: NotRequired[list[Embed]] - allowed_mentions: NotRequired[AllowedMentions] - components: NotRequired[list[Component]] - sticker_ids: NotRequired[list[Snowflake]] - payload_json: NotRequired[str] - attachments: NotRequired[list[Attachment]] - flags: NotRequired[int] diff --git a/pycord/types/role.py b/pycord/types/role.py deleted file mode 100644 index 27a61028..00000000 --- a/pycord/types/role.py +++ /dev/null @@ -1,45 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .snowflake import Snowflake - - -class RoleTags(TypedDict): - bot_id: NotRequired[Snowflake] - integration_id: NotRequired[Snowflake] - premium_subscriber: NotRequired[Literal[None]] - - -class Role(TypedDict): - id: Snowflake - name: str - color: int - hoist: bool - icon: NotRequired[str | None] - unicode_emoji: NotRequired[str | None] - position: int - permissions: str - managed: bool - mentionable: bool - tags: NotRequired[RoleTags] diff --git a/pycord/types/stage_instance.py b/pycord/types/stage_instance.py deleted file mode 100644 index f57b7e19..00000000 --- a/pycord/types/stage_instance.py +++ /dev/null @@ -1,38 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing import Literal - -from typing_extensions import TypedDict - -from .snowflake import Snowflake - -PRIVACY_LEVEL = Literal[1, 2] - - -class StageInstance(TypedDict): - id: Snowflake - guild_id: Snowflake - channel_id: Snowflake - topic: str - privacy_level: PRIVACY_LEVEL - discoverable_disabled: bool - guild_scheduled_event_id: Snowflake | None diff --git a/pycord/types/user.py b/pycord/types/user.py deleted file mode 100644 index 1ea7f055..00000000 --- a/pycord/types/user.py +++ /dev/null @@ -1,117 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING, Literal - -from typing_extensions import NotRequired, TypedDict - -if TYPE_CHECKING: - from .integration import Integration - -from .snowflake import Snowflake - -PREMIUM_TYPE = Literal[0, 1, 2, 3] -LOCALE = Literal[ - 'da', - 'de', - 'en-GB', - 'en-US', - 'en-ES', - 'fr', - 'hr', - 'it', - 'lt', - 'hu', - 'nl', - 'no', - 'pl', - 'pt-BR', - 'ro', - 'fi', - 'sv-SE', - 'vi', - 'tr', - 'cs', - 'el', - 'bg', - 'ru', - 'uk', - 'hi', - 'th', - 'zh-CN', - 'ja', - 'zh-TW', - 'ko', -] - - -class User(TypedDict): - id: Snowflake - username: str - discriminator: str - avatar: str | None - bot: NotRequired[bool] - system: NotRequired[bool] - mfa_enabled: NotRequired[bool] - banner: NotRequired[str | None] - accent_color: NotRequired[int | None] - locale: NotRequired[LOCALE] - verified: NotRequired[bool] - email: NotRequired[str | None] - flags: NotRequired[int] - premium_type: NotRequired[PREMIUM_TYPE] - public_flags: NotRequired[int] - - -SERVICE = Literal[ - 'battlenet', - 'ebay', - 'epicgames', - 'facebook', - 'github', - 'leagueoflegends', - 'paypal', - 'playstation', - 'reddit', - 'riotgames', - 'spotify', - 'skype', - 'steam', - 'twitch', - 'twitter', - 'xbox', - 'youtuve', -] -VISIBILITY = Literal[0, 1] - - -class Connection(TypedDict): - id: str - name: str - type: SERVICE - revoked: NotRequired[bool] - integrations: NotRequired[list[Integration]] - verified: bool - friend_sync: bool - show_activity: bool - two_way_link: bool - visibility: int diff --git a/pycord/types/voice_state.py b/pycord/types/voice_state.py deleted file mode 100644 index 92374be1..00000000 --- a/pycord/types/voice_state.py +++ /dev/null @@ -1,49 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from typing_extensions import NotRequired, TypedDict - -from .guild import GuildMember -from .snowflake import Snowflake - - -class VoiceState(TypedDict): - guild_id: NotRequired[Snowflake] - channel_id: Snowflake | None - user_id: Snowflake - member: NotRequired[GuildMember] - session_id: str - deaf: bool - mute: bool - self_deaf: bool - self_mute: bool - self_stream: NotRequired[bool] - self_video: bool - suppress: bool - request_to_speak_timestamp: str | None - - -class VoiceRegion(TypedDict): - id: str - name: str - optimal: bool - deprecated: bool - custom: bool diff --git a/pycord/types/webhook.py b/pycord/types/webhook.py deleted file mode 100644 index a7d98510..00000000 --- a/pycord/types/webhook.py +++ /dev/null @@ -1,46 +0,0 @@ -# cython: language_level=3 - -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import Literal - -from typing_extensions import NotRequired, TypedDict - -from .channel import Channel -from .guild import Guild -from .snowflake import Snowflake -from .user import User - -WTYPE = Literal[1, 2, 3] - - -class Webhook(TypedDict): - id: Snowflake - type: WTYPE - guild_id: NotRequired[Snowflake | None] - channel_id: NotRequired[Snowflake | None] - user: NotRequired[User] - name: str | None - avatar: str | None - token: NotRequired[str] - application_id: str | None - source_guild: NotRequired[Guild] - source_channel: NotRequired[Channel] - url: NotRequired[str] diff --git a/pycord/types/welcome_screen.py b/pycord/types/welcome_screen.py deleted file mode 100644 index a7862689..00000000 --- a/pycord/types/welcome_screen.py +++ /dev/null @@ -1,35 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2022-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from typing import TypedDict - -from .snowflake import Snowflake - - -class WelcomeScreenChannel(TypedDict): - channel_id: Snowflake - description: str - emoji_id: Snowflake | None - emoji_name: str | None - - -class WelcomeScreen(TypedDict): - description: str | None - welcome_channels: list[WelcomeScreenChannel] diff --git a/pycord/typing.py b/pycord/typing.py deleted file mode 100644 index ef602e8c..00000000 --- a/pycord/typing.py +++ /dev/null @@ -1,47 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - - -import typing -from typing import TYPE_CHECKING, Any - -from .types.snowflake import Snowflake - -if TYPE_CHECKING: - from .state import State - - -class Typing: - __slots__ = ('_state', '__channel_id') - - def __init__(self, channel_id: Snowflake, state: 'State') -> None: - self._state = state - self.__channel_id = channel_id - - async def trigger(self) -> None: - await self._state.http.trigger_typing_indicator(self.__channel_id) - - async def __aenter__(self) -> typing.Self: - await self.trigger() - return self - - async def __aexit__(self, exc_t: Any, exc_v: Any, exc_tb: Any) -> None: - await self.trigger() diff --git a/pycord/ui/__init__.py b/pycord/ui/__init__.py deleted file mode 100644 index 281a65bb..00000000 --- a/pycord/ui/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -pycord.ui -~~~~~~~~~ -Implementation of Discord's UI-based Component Features - -:copyright: 2021-present Pycord Development -:license: MIT -""" -from .button import * -from .component import * -from .house import * -from .interactive_component import * -from .select_menu import * -from .text_input import * diff --git a/pycord/ui/button.py b/pycord/ui/button.py deleted file mode 100644 index 7d42611d..00000000 --- a/pycord/ui/button.py +++ /dev/null @@ -1,99 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from ..enums import ButtonStyle -from ..errors import ComponentException -from ..media import Emoji -from ..missing import MISSING, MissingEnum -from ..types import AsyncFunc -from ..utils import remove_undefined -from .interactive_component import InteractiveComponent - -if TYPE_CHECKING: - from ..interaction import Interaction - - -class Button(InteractiveComponent): - """ - Represents a Discord Button - """ - - def __init__( - self, - callback: AsyncFunc, - # button-based values - style: ButtonStyle | int, - label: str | MissingEnum = MISSING, - custom_id: str | MissingEnum = MISSING, - emoji: str | Emoji | MissingEnum = MISSING, - url: str | MissingEnum = MISSING, - disabled: bool = False, - ) -> None: - super().__init__(callback, custom_id) - if isinstance(style, ButtonStyle): - self._style = style.value - else: - self._style = style - self.style = style - self.label: str | None | MissingEnum = label - self.url = url - - if label is None and url is None: - raise ComponentException('label and url cannot both be None') - - if url and custom_id: - raise ComponentException('Cannot have custom_id and url at the same time') - - if label is None: - self.label = MISSING - - if isinstance(emoji, str): - self.emoji = Emoji._from_str(emoji, None) - else: - self.emoji = emoji - - self.disabled = disabled - - def _to_dict(self) -> dict[str, Any]: - return remove_undefined( - **{ - 'style': self._style, - 'label': self.label, - 'url': self.url, - 'custom_id': self.id, - 'emoji': self.emoji._partial() if self.emoji else MISSING, - 'disabled': self.disabled, - 'type': 2, - } - ) - - def disable(self) -> None: - """ - Disables this Button - """ - self.disabled = True - - async def _internal_invocation(self, inter: Interaction) -> None: - await self._callback(inter) diff --git a/pycord/ui/component.py b/pycord/ui/component.py deleted file mode 100644 index c1c27448..00000000 --- a/pycord/ui/component.py +++ /dev/null @@ -1,59 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from copy import copy -from dataclasses import dataclass, field -from typing import Any, Literal - - -class Component: - """ - The base component type which every other component - subclasses and bases off of - """ - - id: str - type: int - disabled: bool - - def copy(self) -> Component: - return copy(self) - - def _to_dict(self) -> dict[str, Any]: - ... - - def disable(self) -> None: - ... - - -@dataclass -class ActionRow: - """ - Represents a Discord Action Row - """ - - type: Literal[1] = field(default=1) - components: list[Component] = field(default=list) - - def _to_dict(self) -> dict[str, Any]: - return {'type': self.type, 'components': self.components} diff --git a/pycord/ui/house.py b/pycord/ui/house.py deleted file mode 100644 index 73e93f63..00000000 --- a/pycord/ui/house.py +++ /dev/null @@ -1,185 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from copy import copy -from typing import Literal -from uuid import uuid4 - -from ..enums import ButtonStyle, SelectMenuType -from ..errors import ComponentException -from ..media import Emoji -from ..missing import MISSING, MissingEnum -from ..types import AsyncFunc -from .button import Button -from .component import ActionRow, Component -from .select_menu import SelectMenu - - -class House: - """ - The house for components. - You can have, at maximum, **five** houses on one message - """ - - def __init__(self) -> None: - self.components: dict[Component, Component] = {} - - def disabled(self) -> House: - """ - Returns a copy of this House in which all components are disabled - """ - c = copy(self) - c.components = {} - - for _, comp in self.components.items(): - cc = copy(comp) - cc.disable() - c.components[cc] = cc - - return c - - def action_row(self) -> ActionRow: - """ - A representation of this house within an action row. - """ - return ActionRow(components=[c._to_dict() for _, c in self.components.items()]) - - def add_component(self, comp: Component) -> None: - """ - Append a component to this House's store - - Parameters - ---------- - comp: :class:`.Component` - The component to append - """ - if len(self.components) == 5: - raise ComponentException( - 'Cannot add more components, already reached maximum' - ) - - self.components[comp] = comp - - def remove_component(self, comp: Component) -> None: - """ - Remove a component from this House's store - - Parameters - ---------- - comp: :class:`.Component` - The component to remove - """ - del self.components[comp] - - def button( - self, - style: ButtonStyle | int, - label: str | None, - emoji: str | Emoji | MissingEnum = MISSING, - url: str | MissingEnum = MISSING, - disabled: bool = False, - ) -> Button: - """ - Create a new button within this house - - Parameters - ---------- - style: Union[:class:`.ButtonStyle`, :class:`int`] - The style of button to use - label: Union[:class:`str`, None] - The label to use for this button - emoji: Union[:class:`str`, :class:`.Emoji`] - The emoji to use in front of this button's label - url: :class:`str` - The URL of this button - disabled: :class:`bool` - Wether if this button shall be started disabled or not. - Defaults to `False`. - """ - - def wrapper(func: AsyncFunc) -> Button: - if not url: - custom_id = str(uuid4()) - else: - custom_id = MISSING - button = Button( - func, - style=style, - label=label, - emoji=emoji, - url=url, - disabled=disabled, - custom_id=custom_id, - ) - self.add_component(button) - return button - - return wrapper - - def select_menu( - self, - type: Literal[3, 5, 6, 7, 8] | SelectMenuType = 3, - channel_types: list[int] | MissingEnum = MISSING, - placeholder: str | MissingEnum = MISSING, - min_values: int | MissingEnum = MISSING, - max_values: int | MissingEnum = MISSING, - disabled: bool | MissingEnum = MISSING, - ) -> SelectMenu: - """ - Create a new Select Menu within this House - - Parameters - ---------- - type: Union[:class:`int`, :class:`.SelectMenuType`] - The type of Select Menu to instantiate - channel_types: list[:class:`int`] - A list of channel types to limit this select menu to - placeholder: :class:`str` - The placeholder value to put - min_values: :class:`int` - The minimum number of values allowed - max_values: :class:`int` - The maximum number of values allowed - disabled: :class:`bool` - Wether if this select menu shall be started disabled or not. - Defaults to `False`. - """ - if isinstance(type, SelectMenuType): - type = type.value - - def wrapper(func: AsyncFunc) -> SelectMenu: - custom_id = str(uuid4()) - select = SelectMenu( - func, - custom_id=custom_id, - type=type, - channel_types=channel_types, - placeholder=placeholder, - min_values=min_values, - max_values=max_values, - disabled=disabled, - ) - self.add_component(select) - return select - - return wrapper diff --git a/pycord/ui/interactive_component.py b/pycord/ui/interactive_component.py deleted file mode 100644 index 12f7fdd0..00000000 --- a/pycord/ui/interactive_component.py +++ /dev/null @@ -1,66 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .component import Component - -if TYPE_CHECKING: - from ..interaction import Interaction - from ..state import State - from ..types import AsyncFunc - - -class InteractiveComponent(Component): - """ - The base set of a component which can be interacted with. - - .. WARNING:: - This is a **base class** which means we don't - recommend usage of it unless you're making your - own component class. - """ - - def __init__( - self, - callback: AsyncFunc, - custom_id: str | None, - ) -> None: - self._callback = callback - self.id = custom_id - self._state: State | None = None - - def _set_state(self, state: State) -> None: - self._state = state - - async def _internal_invocation(self, inter: Interaction) -> None: - ... - - async def _invoke(self, inter: Interaction) -> None: - if inter.type not in (3, 5): - return - - custom_id = inter.data['custom_id'] - - if custom_id == self.id: - await self._internal_invocation(inter) diff --git a/pycord/ui/select_menu.py b/pycord/ui/select_menu.py deleted file mode 100644 index cb3e863b..00000000 --- a/pycord/ui/select_menu.py +++ /dev/null @@ -1,224 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import Any, Literal - -from ..channel import identify_channel -from ..errors import ComponentException -from ..interaction import Interaction -from ..media import Emoji -from ..missing import MISSING, MissingEnum -from ..role import Role -from ..types import AsyncFunc -from ..user import User -from ..utils import get_arg_defaults, remove_undefined -from .interactive_component import InteractiveComponent - - -class SelectOption: - """ - Create a new Select Menu Option - - Parameters - ---------- - label: :class:`str` - The label of this option - description: :class:`str` - The description of this option - emoji: Union[:class:`str`, :class:`.Emoji`] - The emoji to use in front of this option's label - default: :class:`bool` - Wether this is the default option or not. - Defaults to False - """ - - def __init__( - self, - label: str, - description: str | MissingEnum = MISSING, - emoji: str | Emoji | MissingEnum = MISSING, - default: bool | MissingEnum = MISSING, - ) -> None: - self.label = label - self.description = description - self.default = default - self.value: str | None = None - if isinstance(emoji, str): - self.emoji = Emoji._from_str(emoji, None) - else: - self.emoji = emoji - - self._resp: str | None = None - - def _to_dict(self) -> dict[str, Any]: - return remove_undefined( - label=self.label, - description=self.description, - emoji=self.emoji._partial() if self.emoji else MISSING, - value=self.value, - default=self.default, - ) - - def __get__(self) -> str: - return self._resp - - def __bool__(self) -> bool: - return self._resp is not None - - -class SelectMenu(InteractiveComponent): - """ - Represents a Discord Select Menu - """ - - def __init__( - self, - callback: AsyncFunc, - custom_id: str, - type: Literal[3, 5, 6, 7, 8] = 3, - channel_types: list[int] | MissingEnum = MISSING, - placeholder: str | MissingEnum = MISSING, - min_values: int | MissingEnum = MISSING, - max_values: int | MissingEnum = MISSING, - disabled: bool | MissingEnum = MISSING, - ) -> None: - super().__init__(callback, custom_id) - self.type = type - self.options: list[SelectOption] = [] - self.channel_types = channel_types - self.placeholder = placeholder - self.min_values = min_values - self.max_values = max_values - self.disabled = disabled - - # per-type checks - if channel_types and type != 8: - raise ComponentException( - 'channel_types must only be put on type eight select menus' - ) - - self._options_dict: dict[str, Any] = {} - self.parse_arguments() - - def _to_dict(self) -> dict[str, Any]: - return remove_undefined( - type=self.type, - custom_id=self.id, - channel_types=self.channel_types, - placeholder=self.placeholder, - min_values=self.min_values, - max_values=self.max_values, - disabled=self.disabled, - options=[option._to_dict() for option in self.options], - ) - - def parse_arguments(self) -> None: - defaults = get_arg_defaults(self._callback) - - for name, arg in defaults.items(): - if not isinstance(arg[0], SelectOption) and arg[1] != Interaction: - print(arg[0]) - raise ComponentException( - 'Parameters on Select Menu callbacks must only be SelectOptions and Interaction' - ) - elif self.type != 3 and isinstance(arg[0], SelectOption): - raise ComponentException( - 'Options may only be put on type three Select Menus' - ) - - if arg[1] == Interaction: - continue - - option = arg[0] - option.value = name - self.options.append(option) - self._options_dict[name] = option - - async def _internal_invocation(self, inter: Interaction) -> None: - if self.type == 3: - invocation_data: dict[str, bool] = {} - for value in inter.values: - invocation_data[value] = True - for option in self.options: - if option.value not in invocation_data: - invocation_data[option.value] = False - - await self._callback(inter, **invocation_data) - elif self.type == 5: - users = [] - for user_id in inter.values: - users.append( - User(inter.data['resolved']['users'][user_id], self._state) - ) - - await self._callback( - inter, users if len(users) > 1 and users != [] else users[0] - ) - elif self.type == 6: - roles = [] - for role_id in inter.values: - roles.append( - Role(inter.data['resolved']['roles'][role_id], self._state) - ) - - await self._callback( - inter, roles if len(roles) > 1 and roles != [] else roles[0] - ) - elif self.type == 7: - mentionables = [] - for mentionable_id in inter.values: - try: - mentionables.append( - User( - inter.data['resolved']['users'][mentionable_id], self._state - ) - ) - except KeyError: - mentionables.append( - Role(inter.data['resolved']['roles'][mentionable_id]) - ) - - await self._callback( - inter, - mentionables - if len(mentionables) > 1 and mentionables != [] - else mentionables[1], - ) - elif self.type == 8: - channels = [] - for channel_id in inter.values: - channels.append( - identify_channel( - inter.data['resolved']['channels'][channel_id], self._state - ) - ) - - await self._callback( - inter, channels if len(channels) > 1 and channels != [] else channels[0] - ) - - def disable(self) -> None: - """ - Disables this Select Menu - """ - self.disabled = True diff --git a/pycord/ui/text_input.py b/pycord/ui/text_input.py deleted file mode 100644 index 601df597..00000000 --- a/pycord/ui/text_input.py +++ /dev/null @@ -1,159 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE - -from __future__ import annotations - -from typing import Any -from uuid import uuid4 - -from ..enums import TextInputStyle -from ..interaction import Interaction -from ..missing import MISSING, MissingEnum -from ..types import AsyncFunc -from ..utils import remove_undefined -from .interactive_component import InteractiveComponent - - -class Modal: - """ - Represents a Discord Modal - Stores, deletes, uses, and calls Text Inputs - - Parameters - ---------- - title: :class:`str` - The title of this Modal in the Discord UI - """ - - def __init__( - self, - title: str, - ) -> None: - self.id = str(uuid4()) - self.title = title - self.components: list[TextInput] = [] - self._callback: AsyncFunc | None = None - - def on_call(self) -> AsyncFunc: - """ - Add a function to run when this Modal is submitted - """ - - def wrapper(func: AsyncFunc) -> AsyncFunc: - self._callback = func - return func - - return wrapper - - def add_text_input(self, text_input: TextInput) -> None: - """ - Append a Text Input to this Modal - - Parameters - ---------- - text_input: :class:`.TextInput` - The text input to append - """ - self.components.append(text_input) - - def _to_dict(self) -> dict[str, Any]: - return { - 'title': self.title, - 'custom_id': self.id, - 'components': [ - {'type': 1, 'components': [comp._to_dict() for comp in self.components]} - ], - } - - async def _invoke(self, inter: Interaction) -> None: - comb = [] - for text_input in self.components: - found = False - for comp in inter.data['components'][0]['components']: - if comp['custom_id'] == text_input.id: - comb.append(comp['value']) - found = True - if found is False: - comb.append(None) - - await self._callback(inter, *comb) - - -class TextInput(InteractiveComponent): - """ - Represents a Text Input on a Modal - - Parameters - ---------- - label: :class:`str` - The label of this Text Input - style: :class:`style` - The style to use within this Text Input - min_length: :class:`int` - The minimum text length - max_length: :class:`int` - The maximum text length - required: :class:`bool` - Wether this Text Input is required to be filled or not - value: :class:`str` - The default value of this Text Input - placeholder: :class:`str` - The placeholder value to put onto this Text Input - """ - - def __init__( - self, - label: str, - style: TextInputStyle | int, - min_length: int | MissingEnum = MISSING, - max_length: int | MissingEnum = MISSING, - required: bool | MissingEnum = MISSING, - value: str | MissingEnum = MISSING, - placeholder: str | MissingEnum = MISSING, - ) -> None: - self.id = str(uuid4()) - self.label = label - - if isinstance(style, TextInputStyle): - self._style = style.value - self.style = style - else: - self._style = style - self.style = style - - self.min_length = min_length - self.max_length = max_length - self.required = required - self.value = value - self.placeholder = placeholder - - def _to_dict(self) -> dict[str, Any]: - return remove_undefined( - type=4, - custom_id=self.id, - label=self.label, - style=self._style, - min_length=self.min_length, - max_length=self.max_length, - required=self.required, - value=self.value, - placeholder=self.placeholder, - ) diff --git a/pycord/user.py b/pycord/user.py index 72296218..d1c10356 100644 --- a/pycord/user.py +++ b/pycord/user.py @@ -1,5 +1,5 @@ # cython: language_level=3 -# Copyright (c) 2021-present Pycord Development +# Copyright (c) 2022-present Pycord Development # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -18,58 +18,118 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE -from __future__ import annotations -from functools import cached_property from typing import TYPE_CHECKING -from .color import Color +from pycord.channel import DMChannel + +from .asset import Asset from .enums import PremiumType from .flags import UserFlags -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import LOCALE, User as DiscordUser +from .missing import Maybe, MISSING +from .mixins import Identifiable if TYPE_CHECKING: + from discord_typings import UserData + from .state import State +__all__ = ( + "User", +) -class User: - def __init__(self, data: DiscordUser, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.name: str = data['username'] - self.discriminator: str = data['discriminator'] - self._avatar: str | None = data['avatar'] - self.bot: bool | MissingEnum = data.get('bot', MISSING) - self.system: bool | MissingEnum = data.get('system', MISSING) - self.mfa_enabled: bool | MissingEnum = data.get('mfa_enabled', MISSING) - self._banner: MissingEnum | str | None = data.get('banner', MISSING) - self._accent_color: MissingEnum | int | None = data.get('accent_color', MISSING) - self.accent_color: MissingEnum | Color | None = ( - Color(self._accent_color) - if self._accent_color not in [MISSING, None] - else self._accent_color - ) - self.locale: MissingEnum | LOCALE = data.get('locale', MISSING) - self.verified: MissingEnum | bool = data.get('verified', MISSING) - self.email: str | None | MissingEnum = data.get('email', MISSING) - self._flags: MissingEnum | int = data.get('flags', MISSING) - self.flags: MissingEnum | UserFlags = ( - UserFlags.from_value(self._flags) if self._flags is not MISSING else MISSING - ) - self._premium_type: MissingEnum | int = data.get('premium_type', MISSING) - self.premium_type: PremiumType | MissingEnum = ( - PremiumType(self._premium_type) - if self._premium_type is not MISSING - else MISSING - ) - self._public_flags: MissingEnum | int = data.get('public_flags', MISSING) - self.public_flags: MissingEnum | UserFlags = ( - UserFlags.from_value(self._public_flags) - if self._public_flags is not MISSING - else MISSING + +class User(Identifiable): + __slots__ = ( + "_state", + "id", + "username", + "discriminator", + "global_name", + "avatar_hash", + "bot", + "system", + "mfa_enabled", + "banner_hash", + "accent_color", + "locale", + "verified", + "email", + "flags", + "premium_type", + "public_flags", + "avatar_decoration_hash", + ) + + def __init__(self, data: "UserData", state: "State") -> None: + self._state: "State" = state + self._update(data) + + def __repr__(self) -> str: + return ( + f"" ) - @cached_property + def __str__(self) -> str: + return self.global_name or f"{self.username}#{self.discriminator}" + + def _update(self, data: "UserData") -> None: + self.username = data["username"] + self.discriminator = data["discriminator"] + self.global_name = data["global_name"] + self.avatar_hash = data["avatar"] + self.bot = data.get("bot", MISSING) + self.system = data.get("system", MISSING) + self.mfa_enabled = data.get("mfa_enabled", MISSING) + self.banner_hash = data.get("banner", MISSING) + self.accent_color = data.get("accent_color", MISSING) + self.locale = data.get("locale", MISSING) + self.verified = data.get("verified", MISSING) + self.email = data.get("email", MISSING) + self.flags = UserFlags.from_value(data["flags"]) if "flags" in data else MISSING + self.premium_type = PremiumType(data["premium_type"]) if "premium_type" in data else MISSING + self.public_flags = UserFlags.from_value(data["public_flags"]) if "public_flags" in data else MISSING + self.avatar_decoration_hash = data.get("avatar_decoration", MISSING) + + @property def mention(self) -> str: - return f'<@{self.id}>' + return f"<@{self.id}>" + + @property + def display_name(self) -> str: + if self.discriminator != "0": + return f"{self.username}#{self.discriminator}" + return self.global_name or self.username + + @property + def avatar(self) -> Asset | None: + if self.avatar_hash: + return Asset.from_user_avatar(self._state, self.id, self.avatar_hash) + + @property + def default_avatar(self) -> Asset: + index = (int(self.discriminator) % 5) if self.discriminator != "0" else (self.id >> 22) % 6 + return Asset.from_default_user_avatar(self._state, index) + + @property + def display_avatar(self) -> Asset: + return self.avatar or self.default_avatar + + @property + def banner(self) -> Asset | None: + return Asset.from_user_banner(self._state, self.id, self.banner_hash) if self.banner_hash else None + + @property + def avatar_decoration(self) -> Asset | None: + return Asset.from_user_avatar_decoration( + self._state, self.id, self.avatar_decoration_hash + ) if self.avatar_decoration_hash else None + + async def create_dm(self) -> DMChannel: + # TODO: implement + raise NotImplementedError + + async def send(self, *args, **kwargs) -> Message: + dm = await self.create_dm() + return await dm.send(*args, **kwargs) diff --git a/pycord/utils.py b/pycord/utils.py index 95fe3074..ede18cda 100644 --- a/pycord/utils.py +++ b/pycord/utils.py @@ -31,61 +31,66 @@ TYPE_CHECKING, Annotated, Any, + AnyStr, AsyncGenerator, + AsyncIterator, Callable, + Protocol, Type, TypeVar, + cast, get_origin, ) from aiohttp import ClientResponse +from .custom_types import AsyncFunc from .file import File from .missing import MISSING -from .types import AsyncFunc try: import msgspec + + HAS_MSGSPEC = True except ImportError: import json - msgspec = None - -if TYPE_CHECKING: - from .commands.application import ApplicationCommand + HAS_MSGSPEC = False DISCORD_EPOCH: int = 1420070400000 -S = TypeVar('S', bound=Sequence) -T = TypeVar('T') +S = TypeVar("S", bound=Sequence[int]) +T = TypeVar("T") async def _text_or_json(cr: ClientResponse) -> str | dict[str, Any]: - if cr.content_type == 'application/json': - return await cr.json(encoding='utf-8', loads=loads) - return await cr.text('utf-8') + if cr.content_type == "application/json": + return cast(dict[str, Any], await cr.json(encoding="utf-8", loads=loads)) + return await cr.text("utf-8") def loads(data: Any) -> Any: - return msgspec.json.decode(data.encode()) if msgspec else json.loads(data) + return msgspec.json.decode(data.encode()) if HAS_MSGSPEC else json.loads(data) def dumps(data: Any) -> str: - return msgspec.json.encode(data).decode('utf-8') if msgspec else json.dumps(data) + return ( + msgspec.json.encode(data).decode("utf-8") if HAS_MSGSPEC else json.dumps(data) + ) def parse_errors(errors: dict[str, Any], key: str | None = None) -> dict[str, str]: ret = [] for k, v in errors.items(): - kie = f'{k}.{key}' if key else k + kie = f"{k}.{key}" if key else k if isinstance(v, dict): try: - errors_ = v['_errors'] + errors_ = v["_errors"] except KeyError: continue else: - ret.append((kie, ''.join(x.get('message') for x in errors_))) + ret.append((kie, "".join(x.get("message") for x in errors_))) else: ret.append((kie, v)) @@ -115,11 +120,11 @@ def chunk(items: S, n: int) -> Iterator[S]: yield items[start:end] # type: ignore -def remove_undefined(**kwargs) -> dict[str, Any]: +def remove_undefined(**kwargs: Any) -> dict[str, Any]: return {k: v for k, v in kwargs.items() if v is not MISSING} -async def get_iterated_data(iterator: AsyncGenerator) -> list[Any]: +async def get_iterated_data(iterator: AsyncIterator[T]) -> list[T]: hold = [] async for data in iterator: @@ -128,13 +133,13 @@ async def get_iterated_data(iterator: AsyncGenerator) -> list[Any]: return hold -def get_arg_defaults(fnc: AsyncFunc) -> dict[str, tuple[Any, Any]]: +def get_arg_defaults(fnc: AsyncFunc[Any]) -> dict[str, tuple[Any, Any]]: signature = inspect.signature(fnc) ret = {} for k, v in signature.parameters.items(): if ( - v.default is not inspect.Parameter.empty - and v.annotation is not inspect.Parameter.empty + v.default is not inspect.Parameter.empty + and v.annotation is not inspect.Parameter.empty ): ret[k] = (v.default, v.annotation) elif v.default is not inspect.Parameter.empty: @@ -147,7 +152,7 @@ def get_arg_defaults(fnc: AsyncFunc) -> dict[str, tuple[Any, Any]]: return ret -async def find(cls: Type[T], *args: Any, **kwargs: Any) -> T: +async def find(cls: Any, name: str, *args: Any, type: T, **kwargs: Any) -> T: """ Locates an object by either getting it from the cache, or fetching it from the API. @@ -181,19 +186,21 @@ async def find(cls: Type[T], *args: Any, **kwargs: Any) -> T: A single non-Type variant of T in `cls`. """ - if not hasattr(cls, 'fetch') or not hasattr(cls, 'get'): - raise RuntimeError('This class has no get or fetch function') + if not hasattr(cls, "fetch_" + name) or not hasattr(cls, "get_" + name): + raise RuntimeError("This class has no get or fetch function") - mret = await cls.get(*args, **kwargs) + mret = await getattr(cls, "get_" + name)(*args, **kwargs) if mret is None: - return await cls.fetch(*args, **kwargs) + return cast(T, await getattr(cls, "fetch_" + name)(*args, **kwargs)) else: - return mret + return cast(T, mret) # these two (@deprecated & @experimental) are mostly added for the future -def deprecated(alternative: str | None = None, removal: str | None = None): +def deprecated( + alternative: str | None = None, removal: str | None = None +) -> Callable[[AsyncFunc[Any]], Callable[..., AsyncFunc[Any]]]: """ Used to show that a provided API is in its deprecation period. @@ -205,16 +212,16 @@ def deprecated(alternative: str | None = None, removal: str | None = None): Planned removal version. """ - def wrapper(func: Callable): + def wrapper(func: AsyncFunc[Any]) -> Callable[..., AsyncFunc[Any]]: @functools.wraps(func) - def decorator(*args, **kwargs): - message = f'{func.__name__} has been deprecated.' + def decorator(*args: Any, **kwargs: Any) -> Any: + message = f"{func.__name__} has been deprecated." if alternative: - message += f' You can use {alternative} instead.' + message += f" You can use {alternative} instead." if removal: - message += f' This feature will be removed by version {removal}.' + message += f" This feature will be removed by version {removal}." warnings.warn(message, DeprecationWarning, 3) return func(*args, **kwargs) @@ -224,15 +231,15 @@ def decorator(*args, **kwargs): return wrapper -def experimental(): +def experimental() -> Callable[[AsyncFunc[Any]], Callable[..., AsyncFunc[Any]]]: """ Used for showing that a provided API is still experimental. """ - def wrapper(func: Callable): + def wrapper(func: AsyncFunc[Any]) -> Callable[..., AsyncFunc[Any]]: @functools.wraps(func) - def decorator(*args, **kwargs): - message = f'{func.__name__} is an experimental feature, it may be removed at any time and breaking changes can happen without warning.' + def decorator(*args: Any, **kwargs: Any) -> Any: + message = f"{func.__name__} is an experimental feature, it may be removed at any time and breaking changes can happen without warning." warnings.warn(message, DeprecationWarning, 3) return func(*args, **kwargs) @@ -242,7 +249,7 @@ def decorator(*args, **kwargs): return wrapper -def find_mimetype(data: bytes): +def find_mimetype(data: bytes) -> str: """ Gets the mimetype of the given bytes. @@ -252,16 +259,16 @@ def find_mimetype(data: bytes): The image mime type is not supported """ - if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): - return 'image/png' - elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'): - return 'image/jpeg' - elif data.startswith((b'\x47\x49\x46\x38\x37\x61', b'\x47\x49\x46\x38\x39\x61')): - return 'image/gif' - elif data.startswith(b'RIFF') and data[8:12] == b'WEBP': - return 'image/webp' + if data.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"): + return "image/png" + elif data[0:3] == b"\xff\xd8\xff" or data[6:10] in (b"JFIF", b"Exif"): + return "image/jpeg" + elif data.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")): + return "image/gif" + elif data.startswith(b"RIFF") and data[8:12] == b"WEBP": + return "image/webp" else: - raise ValueError('Unsupported image mime type given') + raise ValueError("Unsupported image mime type given") def to_datauri(f: File) -> str: @@ -283,26 +290,26 @@ def to_datauri(f: File) -> str: b64 = base64.b64encode(b) - return f'data:{m};base64,{b64}' + return f"data:{m};base64,{b64.decode()}" def get_args(annotation: Any) -> tuple[Any, ...]: if get_origin(annotation) is not Annotated: raise ValueError( - f'Argument annotation {annotation} must originate from typing.Annotated' + f"Argument annotation {annotation} must originate from typing.Annotated" ) anns = annotation.__args__ + annotation.__metadata__ if len(anns) != 2: raise ValueError( - f'Annotation {annotation} must only have two arguments subsequently' + f"Annotation {annotation} must only have two arguments subsequently" ) - return anns + return cast(tuple[Any, ...], anns) -def dict_compare(d1: dict, d2: dict) -> bool: +def dict_compare(d1: dict[str, Any], d2: dict[str, Any]) -> bool: for n, v in d1.items(): if d2.get(n) != v: return False @@ -310,54 +317,19 @@ def dict_compare(d1: dict, d2: dict) -> bool: return True -def compare_application_command(cmd: ApplicationCommand, raw: dict[str, Any]) -> bool: - if cmd.default_member_permissions != raw['default_member_permissions']: - return False - elif not dict_compare(cmd.name_localizations, raw['name_localizations']): - return False - elif cmd.description != raw['description']: - return False - elif not dict_compare( - cmd.description_localizations, raw['description_localizations'] - ): - return False - elif cmd.dm_permission != raw['dm_permisison']: - return False - elif cmd.nsfw != raw['nsfw']: - return False - - raw_options = {opt['name']: opt for opt in raw['options']} - - for option in cmd.options: - raw = raw_options.get(option.name) - - if raw is None: - return False +def form_qs(path: str, **queries: Any) -> str: + num = -1 + for k, v in queries.items(): + if v is MISSING: + continue - if not dict_compare( - option.name_localizations, raw.get('name_localizations', MISSING) - ): - return False - elif option.description != raw.get('description', MISSING): - return False - elif not dict_compare( - option.description_localizations, - raw.get('description_localziations', MISSING), - ): - return False - elif option.channel_types != raw.get('channel_types', MISSING): - return False - elif option.autocomplete != raw.get('autocomplete', MISSING): - return False - elif option.choices != raw.get('choices', MISSING): - return False - elif option.focused != raw.get('focused', MISSING): - return False - elif option.required != raw.get('required', MISSING): - return False - elif option.min_value != raw.get('min_value', MISSING): - return False - elif option.max_value != raw.get('max_value', MISSING): - return False + num += 1 - return True + if num == 0: + prefix = "?" + else: + prefix = "&" + + path += prefix + f"{k}={v}" + + return path diff --git a/pycord/voice.py b/pycord/voice.py deleted file mode 100644 index 30f12b9e..00000000 --- a/pycord/voice.py +++ /dev/null @@ -1,63 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING - -from .member import Member -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import VoiceState as DiscordVoiceState - -if TYPE_CHECKING: - from .state import State - - -class VoiceState: - def __init__(self, data: DiscordVoiceState, state: State) -> None: - self.guild_id: Snowflake | MissingEnum = ( - Snowflake(data['guild_id']) if data.get('guild_id') is not None else MISSING - ) - self.channel_id: Snowflake | None = ( - Snowflake(data['channel_id']) - if data.get('channel_id') is not None - else None - ) - self.user_id: Snowflake = Snowflake(data['user_id']) - self.member: Member | MissingEnum = ( - Member(data['member'], state, guild_id=self.guild_id) - if data.get('member') is not None - else MISSING - ) - self.session_id: str = data['session_id'] - self.deaf: bool = data['deaf'] - self.mute: bool = data['mute'] - self.self_deaf: bool = data['self_deaf'] - self.self_mute: bool = data['self_mute'] - self.self_stream: bool | MissingEnum = data.get('self_stream', MISSING) - self.self_video: bool = data['self_video'] - self.suppress: bool = data['suppress'] - self.request_to_speak: datetime | MissingEnum = ( - datetime.fromisoformat(data['request_to_speak_timestamp']) - if data.get('request_to_speak_timestamp') is not None - else None - ) diff --git a/pycord/webhook.py b/pycord/webhook.py deleted file mode 100644 index 1e6ee16f..00000000 --- a/pycord/webhook.py +++ /dev/null @@ -1,123 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .api import HTTPClient, Route -from .embed import Embed -from .enums import WebhookType -from .guild import Guild -from .missing import MISSING, Maybe, MissingEnum -from .snowflake import Snowflake -from .types import Webhook as DiscordWebhook -from .user import User -from .utils import remove_undefined - -if TYPE_CHECKING: - from .state import State - - -class Webhook: - def __init__(self, id: int, token: str) -> None: - self.id = Snowflake(id) - self.token = token - self._http = HTTPClient() - - async def send( - self, - content: str | MissingEnum = MISSING, - username: str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - sticker_ids: list[Snowflake] | MissingEnum = MISSING, - flags: int | MissingEnum = MISSING, - ): - await self.execute( - content=content, - username=username, - tts=tts, - embeds=embeds, - sticker_ids=sticker_ids, - flags=flags, - ) - - async def execute( - self, - content: str | MissingEnum = MISSING, - username: str | MissingEnum = MISSING, - tts: bool | MissingEnum = MISSING, - embeds: list[Embed] | MissingEnum = MISSING, - sticker_ids: list[Snowflake] | MissingEnum = MISSING, - flags: int | MissingEnum = MISSING, - ): - if embeds is not MISSING: - embeds = [embed._to_data() for embed in embeds] - - return await self._http.request( - 'POST', - Route( - '/webhooks/{webhook_id}/{webhook_token}', - webhook_id=self.id, - webhook_token=self.token, - ), - data=remove_undefined( - content=content, - embeds=embeds, - username=username, - tts=tts, - sticker_ids=sticker_ids, - flags=flags, - ), - ) - - -class GuildWebhook: - def __init__(self, data: DiscordWebhook, state: State) -> None: - self.id: Snowflake = Snowflake(data['id']) - self.type: WebhookType = WebhookType(data['type']) - self.guild_id: Snowflake | None | MissingEnum = ( - Snowflake(data['guild_id']) - if data.get('guild_id') is not None - else data.get('guild_id', MISSING) - ) - self.channel_id: Snowflake | None | MissingEnum = ( - Snowflake(data['channel_id']) - if data.get('channel_id') is not None - else None - ) - self.user: User | MissingEnum = ( - User(data['user'], state) if data.get('user') is not None else MISSING - ) - self.name: str | None = data['name'] - self._avatar: str | None = data['avatar'] - self.token: str | MissingEnum = data.get('token', MISSING) - self.application_id: Snowflake | None = ( - Snowflake(data['application_id']) - if data.get('application_id') is not None - else None - ) - self.source_guild: Guild | MissingEnum = ( - Guild(data['source_guild'], state) - if data.get('source_guild') is not None - else MISSING - ) - self.url: str | MissingEnum = data.get('url', MISSING) diff --git a/pycord/welcome_screen.py b/pycord/welcome_screen.py deleted file mode 100644 index ad1d6c2d..00000000 --- a/pycord/welcome_screen.py +++ /dev/null @@ -1,45 +0,0 @@ -# cython: language_level=3 -# Copyright (c) 2021-present Pycord Development -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE -from .snowflake import Snowflake -from .types import ( - WelcomeScreen as DiscordWelcomeScreen, - WelcomeScreenChannel as DiscordWelcomeScreenChannel, -) - - -class WelcomeScreenChannel: - def __init__(self, data: DiscordWelcomeScreenChannel) -> None: - self.channel_id: Snowflake = Snowflake(data['channel_id']) - self.description: str = data['description'] - self.emoji_id: Snowflake | None = ( - Snowflake(data['emoji_id']) if data['emoji_id'] is not None else None - ) - self.emoji_name: str | None = data['emoji_name'] - - -class WelcomeScreen: - def __init__(self, data: DiscordWelcomeScreen) -> None: - self.description: str | None = data['description'] - self.welcome_channels: list[WelcomeScreenChannel] = [] - self.welcome_channels.extend( - WelcomeScreenChannel(welcome_channel) - for welcome_channel in data['welcome_channels'] - ) diff --git a/pyproject.toml b/pyproject.toml index 604c9739..6e6aaa92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,15 @@ -[tool.brunette] -target-version = ["py310", "py311"] -single-quotes = true +[build-system] +requires = [ + "setuptools~=68.1", + "wheel~=0.41", + "mypy~=1.5", + "types-setuptools~=68.1", + "mypy-extensions~=1.0.0" +] +build-backend = "setuptools.build_meta" + +[tool.black] +target-version = ["py311"] [tool.isort] profile = "black" @@ -38,29 +47,12 @@ type = [ ] [tool.mypy] -strict = false -check_untyped_defs = false +strict = true incremental = true -namespace_packages = true -no_implicit_optional = true pretty = true python_version = 3.11 + show_column_numbers = true show_error_codes = true show_error_context = true - -# allowed -allow_untyped_globals = false -allow_redefinition = true - -# disallowed -disallow_untyped_decorators = true -disallow_incomplete_defs = true -disallow_untyped_defs = true - -# warnings -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true +ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 61e2cf37..00000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# used to communicate to Discord's REST and Gateway API via HTTP & WebSockets -aiohttp~=3.8 -# used for colored logging and the colored banner -colorlog~=6.7 -# used for enumerate objects -fastenum~=1.0.4 -# extensions for the typing module -typing-extensions~=4.6.3 diff --git a/requirements/_.txt b/requirements/_.txt index 61e2cf37..1f1ba580 100644 --- a/requirements/_.txt +++ b/requirements/_.txt @@ -3,6 +3,10 @@ aiohttp~=3.8 # used for colored logging and the colored banner colorlog~=6.7 # used for enumerate objects -fastenum~=1.0.4 +fastenum~=1.0 # extensions for the typing module -typing-extensions~=4.6.3 +typing-extensions~=4.6 +# faster json parsing & model parsing +msgspec~=0.18.2 +# discord api types +discord-typings~=0.6 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 264b18f2..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[tool:brunette] -target-version = py310 -single-quotes = true \ No newline at end of file diff --git a/setup.py b/setup.py index 2bdc2b11..24b2fe30 100644 --- a/setup.py +++ b/setup.py @@ -19,81 +19,98 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + +import os +from glob import glob + +# import mypyc.build import setuptools -__version__ = '3.0.0' +__version__ = "3.0.0" -with open('requirements.txt') as f: +requirements = [] +with open("requirements/_.txt") as f: requirements = f.read().splitlines() -packages = [ - 'pycord', - 'pycord.ui', - 'pycord.types', - 'pycord.events', - 'pycord.state', - 'pycord.pages', - 'pycord.api', - 'pycord.api.execution', - 'pycord.gateway', - 'pycord.commands', - 'pycord.commands.application', - 'pycord.api.routers', - 'pycord.ext', - 'pycord.ext.gears', -] - -extra_requires = { - 'speed': [ - 'msgspec~=0.9.1', # Faster alternative to the normal json module. - 'aiodns~=3.0', # included in aiohttp speed. - 'Brotli~=1.0.9', # included in aiohttp speed. - 'ciso8601~=2.2.0', # Faster datetime parsing. - 'faust-cchardet~=2.1.16', # cchardet for python 3.11+ - ], - 'docs': [ - 'sphinx==6.1.3', - 'pydata-sphinx-theme~=0.13', - ], -} +packages: list[str] = [] + + +def scan_dir_for_pkgs(folder: str) -> None: + for fn in os.scandir(folder): + if fn.is_dir(): + packages.append(f.name) + scan_dir_for_pkgs(fn.path) + + +scan_dir_for_pkgs("pycord") + + +def get_extra_requirements() -> dict[str, list[str]]: + extra_requirements: dict[str, list[str]] = {} + for fn in os.scandir("requirements"): + if fn.is_file() and fn.name != "required.txt": + with open(fn) as f: + extra_requirements[fn.name.split(".")[0]] = f.read().splitlines() + return extra_requirements + + +mods = glob("pycord/**/*.py", recursive=True) +# excluded modules +for m in mods: + if "missing.py" in m: + mods.remove(m) + elif "event_manager.py" in m: + mods.remove(m) + elif "flags.py" in m: + mods.remove(m) + setuptools.setup( - name='py-cord', + name="py-cord", version=__version__, packages=packages, package_data={ - 'pycord': ['banner.txt', 'ibanner.txt', 'bin/*.dll'], + "pycord": ["panes/*.txt", "bin/*.dll"], }, project_urls={ - 'Documentation': 'https://docs.pycord.dev', - 'Issue Tracker': 'https://github.com/pycord/pycord-v3/issues', - 'Pull Request Tracker': 'https://github.com/pycord/pycord-v3/pulls', + "Documentation": "https://docs.pycord.dev", + "Issue Tracker": "https://github.com/pycord/pycord-v3/issues", + "Pull Request Tracker": "https://github.com/pycord/pycord-v3/pulls", }, - url='https://github.com/pycord/pycord-v3', - license='MIT', - author='Pycord Development', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', + url="https://github.com/pycord/pycord-v3", + license="MIT", + author="Pycord Development", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", install_requires=requirements, - extras_require=extra_requires, - description='A modern Discord API wrapper for Python', - python_requires='>=3.10', + extras_require=get_extra_requirements(), + description="A modern Discord API wrapper for Python", + python_requires=">=3.11", + # mypyc specific + # TODO! + # py_modules=[], + # ext_modules=mypyc.build.mypycify( + # [ + # "--ignore-missing-imports", + # *mods + # ] + # ), classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation :: CPython', - 'Framework :: AsyncIO', - 'Framework :: aiohttp', - 'Topic :: Communications :: Chat', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', + "Development Status :: 2 - Pre-Alpha", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Framework :: AsyncIO", + "Framework :: aiohttp", + "Topic :: Communications :: Chat", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", ], ) diff --git a/tests/__init__.py b/tests/__init__.py index 237ed30f..5ac48854 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,4 @@ def version(): - assert pycord.__version__ != '2.0.0' + assert pycord.__version__ != "2.0.0"