diff --git a/flask_discord_interactions/__init__.py b/flask_discord_interactions/__init__.py new file mode 100644 index 0000000..c97057a --- /dev/null +++ b/flask_discord_interactions/__init__.py @@ -0,0 +1,15 @@ +from .command import CommandOptionType, SlashCommand +from .context import InteractionContext +from .discord import DiscordInteractions, DiscordInteractionsBlueprint +from .response import InteractionResponseType, InteractionResponse + + +__all__ = [ + CommandOptionType, + SlashCommand, + InteractionContext, + DiscordInteractions, + DiscordInteractionsBlueprint, + InteractionResponseType, + InteractionResponse +] diff --git a/flask_discord_interactions/command.py b/flask_discord_interactions/command.py new file mode 100644 index 0000000..fb84cfb --- /dev/null +++ b/flask_discord_interactions/command.py @@ -0,0 +1,33 @@ +from .context import InteractionContext + + +class CommandOptionType: + SUB_COMMAND = 1 + SUB_COMMAND_GROUP = 2 + STRING = 3 + INTEGER = 4 + BOOLEAN = 5 + USER = 6 + CHANNEL = 7 + ROLE = 8 + + +class SlashCommand: + def __init__(self, command, name, description, options): + self.command = command + self.name = name + self.description = description + self.options = options + + def create_kwargs(self, data): + if "options" not in data["data"]: + return {} + + kwargs = {} + for option in data["data"]["options"]: + kwargs[option["name"]] = option["value"] + return kwargs + + def run(self, discord, app, data): + context = InteractionContext(discord, app, data) + return self.command(context, **self.create_kwargs(data)) diff --git a/flask_discord_interactions/context.py b/flask_discord_interactions/context.py new file mode 100644 index 0000000..c63f021 --- /dev/null +++ b/flask_discord_interactions/context.py @@ -0,0 +1,87 @@ +import requests + +from .response import InteractionResponse + + +class InteractionContext: + class InteractionAuthor: + def __init__(self, data=None): + if data: + self.id = data["user"]["id"] + self.username = data["user"]["username"] + self.discriminator = data["user"]["discriminator"] + self.avatar_hash = data["user"]["avatar"] + self.bot = data["user"].get("bot", False) + self.system = data["user"].get("system", False) + self.mfa_enabled = data["user"].get("mfa_enabled", False) + self.locale = data["user"].get("locale") + self.flags = data["user"].get("flags") + self.premium_type = data["user"].get("premium_type") + self.public_flags = data["user"].get("public_flags") + + self.nick = data["nick"] + self.roles = data["roles"] + self.joined_at = data["joined_at"] + self.premium_since = data.get("premium_since") + self.deaf = data["deaf"] + self.mute = data["mute"] + self.pending = data.get("pending") + + @property + def display_name(self): + return self.nick or self.username + + @property + def avatar_url(self): + return ("https://cdn.discordapp.com/avatars/" + f"{self.id}/{self.avatar_hash}.png") + + def __init__(self, discord, app, data=None): + self.client_id = app.config["DISCORD_CLIENT_ID"] + self.auth_headers = discord.auth_headers(app) + + if data: + self.author = self.InteractionAuthor(data["member"]) + self.id = data["id"] + self.token = data["token"] + self.channel_id = data["channel_id"] + self.guild_id = data["guild_id"] + self.options = data["data"].get("options") + self.command_name = data["data"]["name"] + self.command_id = data["data"]["id"] + + def followup_url(self, message=None): + url = ("https://discord.com/api/v8/webhooks/" + f"{self.client_id}/{self.token}") + if message is not None: + url += f"/messages/{message}" + + return url + + def edit(self, response, message="@original"): + response = InteractionResponse.from_return_value(response) + + response = requests.patch( + self.followup_url(message), + json=response.dump_followup(), + headers=self.auth_headers + ) + response.raise_for_status() + + def delete(self, message="@original"): + response = requests.delete( + self.followup_url(message), + headers=self.auth_headers + ) + response.raise_for_status() + + def send(self, response): + response = InteractionResponse.from_return_value(response) + + response = requests.post( + self.followup_url(), + headers=self.auth_headers, + **response.dump_multipart() + ) + response.raise_for_status() + return response.json()["id"] diff --git a/flask_discord_interactions.py b/flask_discord_interactions/discord.py similarity index 54% rename from flask_discord_interactions.py rename to flask_discord_interactions/discord.py index 27be818..c233ecc 100644 --- a/flask_discord_interactions.py +++ b/flask_discord_interactions/discord.py @@ -1,5 +1,4 @@ import time -import json import requests @@ -8,211 +7,15 @@ from nacl.exceptions import BadSignatureError from nacl.signing import VerifyKey +from .command import SlashCommand +from .response import InteractionResponse, InteractionResponseType + class InteractionType: PING = 1 APPLICATION_COMMAND = 2 -class InteractionResponseType: - PONG = 1 - ACKNOWLEDGE = 2 - CHANNEL_MESSAGE = 3 - CHANNEL_MESSAGE_WITH_SOURCE = 4 - ACKNOWLEDGE_WITH_SOURCE = 5 - - -class CommandOptionType: - SUB_COMMAND = 1 - SUB_COMMAND_GROUP = 2 - STRING = 3 - INTEGER = 4 - BOOLEAN = 5 - USER = 6 - CHANNEL = 7 - ROLE = 8 - - -class InteractionContext: - class InteractionAuthor: - def __init__(self, data=None): - if data: - self.id = data["user"]["id"] - self.username = data["user"]["username"] - self.discriminator = data["user"]["discriminator"] - self.avatar_hash = data["user"]["avatar"] - self.bot = data["user"].get("bot", False) - self.system = data["user"].get("system", False) - self.mfa_enabled = data["user"].get("mfa_enabled", False) - self.locale = data["user"].get("locale") - self.flags = data["user"].get("flags") - self.premium_type = data["user"].get("premium_type") - self.public_flags = data["user"].get("public_flags") - - self.nick = data["nick"] - self.roles = data["roles"] - self.joined_at = data["joined_at"] - self.premium_since = data.get("premium_since") - self.deaf = data["deaf"] - self.mute = data["mute"] - self.pending = data.get("pending") - - @property - def display_name(self): - return self.nick or self.username - - @property - def avatar_url(self): - return ("https://cdn.discordapp.com/avatars/" - f"{self.id}/{self.avatar_hash}.png") - - def __init__(self, discord, app, data=None): - self.client_id = app.config["DISCORD_CLIENT_ID"] - self.auth_headers = discord.auth_headers(app) - - if data: - self.author = self.InteractionAuthor(data["member"]) - self.id = data["id"] - self.token = data["token"] - self.channel_id = data["channel_id"] - self.guild_id = data["guild_id"] - self.options = data["data"].get("options") - self.command_name = data["data"]["name"] - self.command_id = data["data"]["id"] - - def followup_url(self, message=None): - url = ("https://discord.com/api/v8/webhooks/" - f"{self.client_id}/{self.token}") - if message is not None: - url += f"/messages/{message}" - - return url - - def edit(self, response, message="@original"): - response = InteractionResponse.from_return_value(response) - - response = requests.patch( - self.followup_url(message), - json=response.dump_followup(), - headers=self.auth_headers - ) - response.raise_for_status() - - def delete(self, message="@original"): - response = requests.delete( - self.followup_url(message), - headers=self.auth_headers - ) - response.raise_for_status() - - def send(self, response): - response = InteractionResponse.from_return_value(response) - - response = requests.post( - self.followup_url(), - headers=self.auth_headers, - **response.dump_multipart() - ) - response.raise_for_status() - return response.json()["id"] - - -class InteractionResponse: - def __init__(self, content=None, *, tts=False, embed=None, embeds=None, - allowed_mentions={"parse": ["roles", "users", "everyone"]}, - with_source=True, file=None, files=None): - self.content = content - self.tts = tts - - if embed is not None and embeds is not None: - raise ValueError("Specify only one of embed or embeds") - if embed is not None: - embeds = [embed] - self.embeds = embeds - - if file is not None and files is not None: - raise ValueError("Specify only one of file or files") - if file is not None: - files = [file] - self.files = files - - self.allowed_mentions = allowed_mentions - - if self.embeds is not None or self.content is not None: - if with_source: - self.response_type = \ - InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE - else: - self.response_type = InteractionResponseType.CHANNEL_MESSAGE - else: - if with_source: - self.response_type = \ - InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE - else: - self.response_type = InteractionResponseType.ACKNOWLEDGE - - @staticmethod - def from_return_value(result): - if result is None: - return InteractionResponse() - elif isinstance(result, InteractionResponse): - return result - else: - return InteractionResponse(str(result)) - - def dump(self): - return { - "type": self.response_type, - "data": { - "content": self.content, - "tts": self.tts, - "embeds": self.embeds, - "allowed_mentions": self.allowed_mentions - } - } - - def dump_followup(self): - return { - "content": self.content, - "tts": self.tts, - "embeds": self.embeds, - "allowed_mentions": self.allowed_mentions - } - - def dump_multipart(self): - if self.files: - payload_json = json.dumps(self.dump_followup()) - - multipart = [] - for file in self.files: - multipart.append(("file", file)) - - return {"data": {"payload_json": payload_json}, "files": multipart} - else: - return {"json": self.dump_followup()} - - -class SlashCommand: - def __init__(self, command, name, description, options): - self.command = command - self.name = name - self.description = description - self.options = options - - def create_kwargs(self, data): - if "options" not in data["data"]: - return {} - - kwargs = {} - for option in data["data"]["options"]: - kwargs[option["name"]] = option["value"] - return kwargs - - def run(self, discord, app, data): - context = InteractionContext(discord, app, data) - return self.command(context, **self.create_kwargs(data)) - - class DiscordInteractionsBlueprint: def __init__(self): self.discord_commands = {} diff --git a/flask_discord_interactions/response.py b/flask_discord_interactions/response.py new file mode 100644 index 0000000..dd400d6 --- /dev/null +++ b/flask_discord_interactions/response.py @@ -0,0 +1,84 @@ +import json + + +class InteractionResponseType: + PONG = 1 + ACKNOWLEDGE = 2 + CHANNEL_MESSAGE = 3 + CHANNEL_MESSAGE_WITH_SOURCE = 4 + ACKNOWLEDGE_WITH_SOURCE = 5 + + +class InteractionResponse: + def __init__(self, content=None, *, tts=False, embed=None, embeds=None, + allowed_mentions={"parse": ["roles", "users", "everyone"]}, + with_source=True, file=None, files=None): + self.content = content + self.tts = tts + + if embed is not None and embeds is not None: + raise ValueError("Specify only one of embed or embeds") + if embed is not None: + embeds = [embed] + self.embeds = embeds + + if file is not None and files is not None: + raise ValueError("Specify only one of file or files") + if file is not None: + files = [file] + self.files = files + + self.allowed_mentions = allowed_mentions + + if self.embeds is not None or self.content is not None: + if with_source: + self.response_type = \ + InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + else: + self.response_type = InteractionResponseType.CHANNEL_MESSAGE + else: + if with_source: + self.response_type = \ + InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE + else: + self.response_type = InteractionResponseType.ACKNOWLEDGE + + @staticmethod + def from_return_value(result): + if result is None: + return InteractionResponse() + elif isinstance(result, InteractionResponse): + return result + else: + return InteractionResponse(str(result)) + + def dump(self): + return { + "type": self.response_type, + "data": { + "content": self.content, + "tts": self.tts, + "embeds": self.embeds, + "allowed_mentions": self.allowed_mentions + } + } + + def dump_followup(self): + return { + "content": self.content, + "tts": self.tts, + "embeds": self.embeds, + "allowed_mentions": self.allowed_mentions + } + + def dump_multipart(self): + if self.files: + payload_json = json.dumps(self.dump_followup()) + + multipart = [] + for file in self.files: + multipart.append(("file", file)) + + return {"data": {"payload_json": payload_json}, "files": multipart} + else: + return {"json": self.dump_followup()} diff --git a/setup.py b/setup.py index 6bcc279..64f87b4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ description='A Flask extension for Discord slash commands.', long_description=readme, long_description_content_type="text/markdown", - py_modules=['flask_discord_interactions'], + packages=['flask_discord_interactions'], zip_safe=False, include_package_data=True, platforms='any',