From f753271403d66e30eb78ef78b85618bab8f49a15 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 13 Apr 2022 22:23:50 +0300 Subject: [PATCH 1/5] feature (meta): Leo says hi. --- bot/modules/meta/join_message.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/modules/meta/join_message.py b/bot/modules/meta/join_message.py index dc3eaec3..4abd1b1d 100644 --- a/bot/modules/meta/join_message.py +++ b/bot/modules/meta/join_message.py @@ -29,6 +29,10 @@ Best of luck with your studies! @client.add_after_event('guild_join', priority=0) async def post_join_message(client: cmdClient, guild: discord.Guild): + try: + await guild.me.edit(nick="Leo") + except discord.HTTPException: + pass if (channel := guild.system_channel) and channel.permissions_for(guild.me).embed_links: embed = discord.Embed( description=message From cc7c988007068a4f85f29290dc0a1c3d5ed73046 Mon Sep 17 00:00:00 2001 From: Conatum Date: Mon, 18 Apr 2022 12:53:17 +0300 Subject: [PATCH 2/5] feature (interactions): Basic button support. --- bot/core/__init__.py | 2 - bot/meta/__init__.py | 4 + bot/meta/client.py | 1 + bot/meta/interactions/__init__.py | 3 + bot/meta/interactions/components.py | 125 ++++++++++++++++++++++++++ bot/meta/interactions/enums.py | 28 ++++++ bot/meta/interactions/interactions.py | 52 +++++++++++ bot/{core => meta}/patches.py | 91 +++++++++++++++++-- 8 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 bot/meta/interactions/__init__.py create mode 100644 bot/meta/interactions/components.py create mode 100644 bot/meta/interactions/enums.py create mode 100644 bot/meta/interactions/interactions.py rename bot/{core => meta}/patches.py (57%) diff --git a/bot/core/__init__.py b/bot/core/__init__.py index be157adb..9be4f2bd 100644 --- a/bot/core/__init__.py +++ b/bot/core/__init__.py @@ -1,7 +1,5 @@ from . import data # noqa -from . import patches - from .module import module from .lion import Lion from . import blacklists diff --git a/bot/meta/__init__.py b/bot/meta/__init__.py index eab9c7b8..b8dfa3c0 100644 --- a/bot/meta/__init__.py +++ b/bot/meta/__init__.py @@ -1,4 +1,8 @@ from .logger import log, logger + +from . import interactions +from . import patches + from .client import client from .config import conf from .args import args diff --git a/bot/meta/client.py b/bot/meta/client.py index a3f02877..267ffe28 100644 --- a/bot/meta/client.py +++ b/bot/meta/client.py @@ -1,6 +1,7 @@ from discord import Intents from cmdClient.cmdClient import cmdClient +from . import patches from .config import conf from .sharding import shard_number, shard_count from LionContext import LionContext diff --git a/bot/meta/interactions/__init__.py b/bot/meta/interactions/__init__.py new file mode 100644 index 00000000..e7e68854 --- /dev/null +++ b/bot/meta/interactions/__init__.py @@ -0,0 +1,3 @@ +from . import enums +from .interactions import _component_interaction_factory, Interaction, ComponentInteraction +from .components import * diff --git a/bot/meta/interactions/components.py b/bot/meta/interactions/components.py new file mode 100644 index 00000000..a09d7414 --- /dev/null +++ b/bot/meta/interactions/components.py @@ -0,0 +1,125 @@ +import asyncio +import uuid + +from .enums import ButtonStyle, InteractionType + + +""" +Notes: + When interaction is sent, add message info + Add wait_for to Button and SelectMenu + wait_for_interaction for generic + listen=True for the listenables, register with a listener + Need a deregister then as well + + send(..., components=[ActionRow(Button(...))]) + + Automatically ack interaction? DEFERRED_UPDATE_MESSAGE + + async def Button.wait_for(timeout=None, ack=False) + Blocks until the button is pressed. Returns a ButtonPress (Interaction). + def MessageComponent.add_callback(timeout) + Adds an async callback function to the Component. + + Construct the response independent of the original component. + Original component has a convenience wait_for that runs wait_for_interaction(custom_id=self.custom_id)... + The callback? Just add a wait_for +""" + + +class MessageComponent: + _type = None + + def __init_(self, *args, **kwargs): + self.message = None + + def listen(self): + ... + + def close(self): + ... + + +class ActionRow(MessageComponent): + _type = 1 + + def __init__(self, *components): + self.components = components + + def to_dict(self): + data = { + "type": self._type, + "components": [comp.to_dict() for comp in self.components] + } + return data + + +class Button(MessageComponent): + _type = 2 + + def __init__(self, label, style=ButtonStyle.PRIMARY, custom_id=None, url=None, emoji=None, disabled=False): + if style == ButtonStyle.LINK: + if url is None: + raise ValueError("Link buttons must have a url") + custom_id = None + elif custom_id is None: + custom_id = uuid.uuid4() + + self.label = label + self.style = style + self.custom_id = custom_id + self.url = url + + self.emoji = emoji + self.disabled = disabled + + def to_dict(self): + data = { + "type": self._type, + "label": self.label, + "style": int(self.style) + } + if self.style == ButtonStyle.LINK: + data['url'] = self.url + else: + data['custom_id'] = self.custom_id + if self.emoji is not None: + # TODO: This only supports PartialEmoji, not Emoji + data['emoji'] = self.emoji.to_dict() + return data + + async def wait_for_press(self, timeout=None, check=None): + from meta import client + + def _check(interaction): + valid = True + print(interaction.custom_id) + valid = valid and interaction.interaction_type == InteractionType.MESSAGE_COMPONENT + valid = valid and interaction.custom_id == self.custom_id + valid = valid and (check is None or check(interaction)) + return valid + + return await client.wait_for('interaction_create', timeout=timeout, check=_check) + + def on_press(self, timeout=None, repeat=True, pass_args=(), pass_kwargs={}): + def wrapper(func): + async def wrapped(): + while True: + try: + button_press = await self.wait_for_press(timeout=timeout) + except asyncio.TimeoutError: + break + asyncio.create_task(func(button_press, *pass_args, **pass_kwargs)) + if not repeat: + break + future = asyncio.create_task(wrapped()) + return future + return wrapper + + +class SelectMenu(MessageComponent): + _type = 3 + + +# MessageComponent listener +live_components = {} diff --git a/bot/meta/interactions/enums.py b/bot/meta/interactions/enums.py new file mode 100644 index 00000000..c08974be --- /dev/null +++ b/bot/meta/interactions/enums.py @@ -0,0 +1,28 @@ +from enum import IntEnum + + +class InteractionType(IntEnum): + PING = 1 + APPLICATION_COMMAND = 2 + MESSAGE_COMPONENT = 3 + APPLICATION_COMMAND_AUTOCOMPLETE = 4 + MODAL_SUBMIT = 5 + + +class ComponentType(IntEnum): + ACTIONROW = 1 + BUTTON = 2 + SELECTMENU = 3 + TEXTINPUT = 4 + + +class ButtonStyle(IntEnum): + PRIMARY = 1 + SECONDARY = 2 + SUCCESS = 3 + DANGER = 4 + LINK = 5 + + +class InteractionCallback(IntEnum): + DEFERRED_UPDATE_MESSAGE = 6 diff --git a/bot/meta/interactions/interactions.py b/bot/meta/interactions/interactions.py new file mode 100644 index 00000000..661189a9 --- /dev/null +++ b/bot/meta/interactions/interactions.py @@ -0,0 +1,52 @@ +import asyncio +from .enums import ComponentType, InteractionType, InteractionCallback + + +class Interaction: + __slots__ = ( + 'id', + 'token', + '_state' + ) + + async def callback_deferred(self): + return await self._state.http.interaction_callback(self.id, self.token, InteractionCallback.DEFERRED_UPDATE_MESSAGE) + + def ack(self): + asyncio.create_task(self.callback_deferred()) + + +class ComponentInteraction(Interaction): + interaction_type = InteractionType.MESSAGE_COMPONENT + # TODO: Slots + + def __init__(self, message, user, data, state): + self.message = message + self.user = user + + self._state = state + + self._from_data(data) + + def _from_data(self, data): + self.id = data['id'] + self.token = data['token'] + self.application_id = data['application_id'] + + component_data = data['data'] + + self.component_type = ComponentType(component_data['component_type']) + self.custom_id = component_data.get('custom_id', None) + + +class ButtonPress(ComponentInteraction): + __slots__ = () + + +def _component_interaction_factory(data): + component_type = data['data']['component_type'] + + if component_type == ComponentType.BUTTON: + return ButtonPress + else: + return None diff --git a/bot/core/patches.py b/bot/meta/patches.py similarity index 57% rename from bot/core/patches.py rename to bot/meta/patches.py index a292c443..14a5741e 100644 --- a/bot/core/patches.py +++ b/bot/meta/patches.py @@ -1,14 +1,23 @@ """ Temporary patches for the discord.py library to support new features of the discord API. """ +import logging + +from discord.state import ConnectionState from discord.http import Route, HTTPClient from discord.abc import Messageable -from discord.utils import InvalidArgument -from discord import File, AllowedMentions +from discord.utils import InvalidArgument, _get_as_snowflake +from discord import File, AllowedMentions, Member, User, Message + +from .interactions import _component_interaction_factory +from .interactions.enums import InteractionType + + +log = logging.getLogger(__name__) def send_message(self, channel_id, content, *, tts=False, embeds=None, - nonce=None, allowed_mentions=None, message_reference=None): + nonce=None, allowed_mentions=None, message_reference=None, components=None): r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) payload = {} @@ -30,16 +39,37 @@ def send_message(self, channel_id, content, *, tts=False, embeds=None, if message_reference: payload['message_reference'] = message_reference + if components is not None: + payload['components'] = components + + return self.request(r, json=payload) + + +def interaction_callback(self, interaction_id, interaction_token, callback_type, callback_data=None): + r = Route( + 'POST', + '/interactions/{interaction_id}/{interaction_token}/callback', + interaction_id=interaction_id, + interaction_token=interaction_token + ) + + payload = {} + + payload['type'] = int(callback_type) + if callback_data: + payload['data'] = callback_data + return self.request(r, json=payload) HTTPClient.send_message = send_message +HTTPClient.interaction_callback = interaction_callback async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=None, files=None, delete_after=None, nonce=None, allowed_mentions=None, reference=None, - mention_author=None): + mention_author=None, components=None): channel = await self._get_channel() state = self._state @@ -53,6 +83,9 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=N if embeds is not None: embeds = [embed.to_dict() for embed in embeds] + if components is not None: + components = [comp.to_dict() for comp in components] + if allowed_mentions is not None: if state.allowed_mentions is not None: allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict() @@ -101,7 +134,7 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=N else: data = await state.http.send_message(channel.id, content, tts=tts, embeds=embeds, nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference) + message_reference=reference, components=components) ret = state.create_message(channel=channel, data=data) if delete_after is not None: @@ -109,3 +142,51 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=N return ret Messageable.send = send + + +def parse_interaction_create(self, data): + self.dispatch('raw_interaction_create', data) + + if (guild_id := data.get('guild_id', None)): + guild = self._get_guild(int(guild_id)) + if guild is None: + log.debug('INTERACTION_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id) + return + else: + guild = None + + if (member_data := data.get('member', None)) is not None: + # Construct member + # TODO: Theoretical reliance on cached guild + user = Member(data=member_data, guild=guild, state=self) + else: + # Assume user + user = self.get_user(_get_as_snowflake(data['user'], 'id')) or User(data=data['user'], state=self) + + message = self._get_message(_get_as_snowflake(data['message'], 'id')) + if not message: + message_data = data['message'] + channel, _ = self._get_guild_channel(message_data) + message = Message(data=message_data, channel=channel, state=self) + if self._messages is not None: + self._messages.append(message) + + interaction = None + if data['type'] == InteractionType.MESSAGE_COMPONENT: + interaction_class = _component_interaction_factory(data) + if interaction_class: + interaction = interaction_class(message, user, data, self) + else: + log.debug( + 'INTERACTION_CREATE recieved unhandled message component interaction type: %s', + data['data']['component_type'] + ) + else: + log.debug('INTERACTION_CREATE recieved unhandled interaction type: %s', data['type']) + interaction = None + + if interaction: + self.dispatch('interaction_create', interaction) + + +ConnectionState.parse_interaction_create = parse_interaction_create From e73302d21fa8bad41793c6413bdedf5705efb712 Mon Sep 17 00:00:00 2001 From: Conatum Date: Mon, 18 Apr 2022 13:40:38 +0300 Subject: [PATCH 3/5] (interactions): Basic select menu support. --- bot/meta/interactions/components.py | 131 ++++++++++++++------------ bot/meta/interactions/interactions.py | 10 ++ 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/bot/meta/interactions/components.py b/bot/meta/interactions/components.py index a09d7414..a651e241 100644 --- a/bot/meta/interactions/components.py +++ b/bot/meta/interactions/components.py @@ -4,41 +4,12 @@ import uuid from .enums import ButtonStyle, InteractionType -""" -Notes: - When interaction is sent, add message info - Add wait_for to Button and SelectMenu - wait_for_interaction for generic - listen=True for the listenables, register with a listener - Need a deregister then as well - - send(..., components=[ActionRow(Button(...))]) - - Automatically ack interaction? DEFERRED_UPDATE_MESSAGE - - async def Button.wait_for(timeout=None, ack=False) - Blocks until the button is pressed. Returns a ButtonPress (Interaction). - def MessageComponent.add_callback(timeout) - Adds an async callback function to the Component. - - Construct the response independent of the original component. - Original component has a convenience wait_for that runs wait_for_interaction(custom_id=self.custom_id)... - The callback? Just add a wait_for -""" - - class MessageComponent: _type = None def __init_(self, *args, **kwargs): self.message = None - def listen(self): - ... - - def close(self): - ... - class ActionRow(MessageComponent): _type = 1 @@ -54,7 +25,37 @@ class ActionRow(MessageComponent): return data -class Button(MessageComponent): +class AwaitableComponent: + async def wait_for(self, timeout=None, check=None): + from meta import client + + def _check(interaction): + valid = True + print(interaction.custom_id) + valid = valid and interaction.interaction_type == InteractionType.MESSAGE_COMPONENT + valid = valid and interaction.custom_id == self.custom_id + valid = valid and (check is None or check(interaction)) + return valid + + return await client.wait_for('interaction_create', timeout=timeout, check=_check) + + def add_callback(self, timeout=None, repeat=True, pass_args=(), pass_kwargs={}): + def wrapper(func): + async def wrapped(): + while True: + try: + button_press = await self.wait_for(timeout=timeout) + except asyncio.TimeoutError: + break + asyncio.create_task(func(button_press, *pass_args, **pass_kwargs)) + if not repeat: + break + future = asyncio.create_task(wrapped()) + return future + return wrapper + + +class Button(MessageComponent, AwaitableComponent): _type = 2 def __init__(self, label, style=ButtonStyle.PRIMARY, custom_id=None, url=None, emoji=None, disabled=False): @@ -63,7 +64,7 @@ class Button(MessageComponent): raise ValueError("Link buttons must have a url") custom_id = None elif custom_id is None: - custom_id = uuid.uuid4() + custom_id = str(uuid.uuid4()) self.label = label self.style = style @@ -88,38 +89,52 @@ class Button(MessageComponent): data['emoji'] = self.emoji.to_dict() return data - async def wait_for_press(self, timeout=None, check=None): - from meta import client - def _check(interaction): - valid = True - print(interaction.custom_id) - valid = valid and interaction.interaction_type == InteractionType.MESSAGE_COMPONENT - valid = valid and interaction.custom_id == self.custom_id - valid = valid and (check is None or check(interaction)) - return valid +class SelectOption: + def __init__(self, label, value, description, emoji=None, default=False): + self.label = label + self.value = value + self.description = description + self.emoji = emoji + self.default = default - return await client.wait_for('interaction_create', timeout=timeout, check=_check) + def to_dict(self): + data = { + "label": self.label, + "value": self.value, + "description": self.description, + } + if self.emoji: + data['emoji'] = self.emoji.to_dict() + if self.default: + data['default'] = self.default - def on_press(self, timeout=None, repeat=True, pass_args=(), pass_kwargs={}): - def wrapper(func): - async def wrapped(): - while True: - try: - button_press = await self.wait_for_press(timeout=timeout) - except asyncio.TimeoutError: - break - asyncio.create_task(func(button_press, *pass_args, **pass_kwargs)) - if not repeat: - break - future = asyncio.create_task(wrapped()) - return future - return wrapper + return data -class SelectMenu(MessageComponent): +class SelectMenu(MessageComponent, AwaitableComponent): _type = 3 + def __init__(self, *options, custom_id=None, placeholder=None, min_values=None, max_values=None, disabled=False): + self.options = options + self.custom_id = custom_id or str(uuid.uuid4()) + self.placeholder = placeholder + self.min_values = min_values + self.max_values = max_values + self.disabled = disabled -# MessageComponent listener -live_components = {} + def to_dict(self): + data = { + "type": self._type, + 'custom_id': self.custom_id, + 'options': [option.to_dict() for option in self.options], + } + if self.placeholder: + data['placeholder'] = self.placeholder + if self.min_values: + data['min_values'] = self.min_values + if self.max_values: + data['max_values'] = self.max_values + if self.disabled: + data['disabled'] = self.disabled + return data diff --git a/bot/meta/interactions/interactions.py b/bot/meta/interactions/interactions.py index 661189a9..01f5d993 100644 --- a/bot/meta/interactions/interactions.py +++ b/bot/meta/interactions/interactions.py @@ -43,10 +43,20 @@ class ButtonPress(ComponentInteraction): __slots__ = () +class Selection(ComponentInteraction): + __slots__ = ('values',) + + def _from_data(self, data): + super()._from_data(data) + self.values = data['data']['values'] + + def _component_interaction_factory(data): component_type = data['data']['component_type'] if component_type == ComponentType.BUTTON: return ButtonPress + elif component_type == ComponentType.SELECTMENU: + return Selection else: return None From 035a2959625a18434e278f9cfa1640818ebfc07f Mon Sep 17 00:00:00 2001 From: Conatum Date: Tue, 19 Apr 2022 11:34:34 +0300 Subject: [PATCH 4/5] (interactions): Basic support for modals. --- bot/meta/interactions/__init__.py | 3 +- bot/meta/interactions/components.py | 12 +++- bot/meta/interactions/enums.py | 12 ++++ bot/meta/interactions/interactions.py | 56 +++++++++++++++++- bot/meta/interactions/modals.py | 54 ++++++++++++++++++ bot/meta/patches.py | 81 +++++++++++++++++++++++++-- 6 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 bot/meta/interactions/modals.py diff --git a/bot/meta/interactions/__init__.py b/bot/meta/interactions/__init__.py index e7e68854..660b5a93 100644 --- a/bot/meta/interactions/__init__.py +++ b/bot/meta/interactions/__init__.py @@ -1,3 +1,4 @@ from . import enums -from .interactions import _component_interaction_factory, Interaction, ComponentInteraction +from .interactions import _component_interaction_factory, Interaction, ComponentInteraction, ModalResponse from .components import * +from .modals import * diff --git a/bot/meta/interactions/components.py b/bot/meta/interactions/components.py index a651e241..c7788546 100644 --- a/bot/meta/interactions/components.py +++ b/bot/meta/interactions/components.py @@ -1,15 +1,23 @@ import asyncio import uuid +import json from .enums import ButtonStyle, InteractionType class MessageComponent: _type = None + interaction_type = InteractionType.MESSAGE_COMPONENT def __init_(self, *args, **kwargs): self.message = None + def to_dict(self): + raise NotImplementedError + + def to_json(self): + return json.dumps(self.to_dict()) + class ActionRow(MessageComponent): _type = 1 @@ -26,13 +34,15 @@ class ActionRow(MessageComponent): class AwaitableComponent: + interaction_type: InteractionType = None + async def wait_for(self, timeout=None, check=None): from meta import client def _check(interaction): valid = True print(interaction.custom_id) - valid = valid and interaction.interaction_type == InteractionType.MESSAGE_COMPONENT + valid = valid and interaction.interaction_type == self.interaction_type valid = valid and interaction.custom_id == self.custom_id valid = valid and (check is None or check(interaction)) return valid diff --git a/bot/meta/interactions/enums.py b/bot/meta/interactions/enums.py index c08974be..3d1c8c6c 100644 --- a/bot/meta/interactions/enums.py +++ b/bot/meta/interactions/enums.py @@ -24,5 +24,17 @@ class ButtonStyle(IntEnum): LINK = 5 +class TextInputStyle(IntEnum): + SHORT = 1 + PARAGRAPH = 2 + + class InteractionCallback(IntEnum): + PONG = 1 + CHANNEL_MESSAGE_WITH_SOURCE = 4 + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5 DEFERRED_UPDATE_MESSAGE = 6 + UPDATE_MESSAGE = 7 + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8 + MODAL = 9 + diff --git a/bot/meta/interactions/interactions.py b/bot/meta/interactions/interactions.py index 01f5d993..f51df5ed 100644 --- a/bot/meta/interactions/interactions.py +++ b/bot/meta/interactions/interactions.py @@ -9,11 +9,23 @@ class Interaction: '_state' ) - async def callback_deferred(self): - return await self._state.http.interaction_callback(self.id, self.token, InteractionCallback.DEFERRED_UPDATE_MESSAGE) + async def response_deferred(self): + return await self._state.http.interaction_callback( + self.id, + self.token, + InteractionCallback.DEFERRED_UPDATE_MESSAGE + ) + + async def response_modal(self, modal): + return await self._state.http.interaction_callback( + self.id, + self.token, + InteractionCallback.MODAL, + modal.to_dict() + ) def ack(self): - asyncio.create_task(self.callback_deferred()) + asyncio.create_task(self.response_deferred()) class ComponentInteraction(Interaction): @@ -51,6 +63,44 @@ class Selection(ComponentInteraction): self.values = data['data']['values'] +class ModalResponse(Interaction): + __slots__ = ( + 'message', + 'user', + '_state' + 'id', + 'token', + 'application_id', + 'custom_id', + 'values' + ) + + interaction_type = InteractionType.MODAL_SUBMIT + + def __init__(self, message, user, data, state): + self.message = message + self.user = user + + self._state = state + + self._from_data(data) + + def _from_data(self, data): + self.id = data['id'] + self.token = data['token'] + self.application_id = data['application_id'] + + component_data = data['data'] + + self.custom_id = component_data.get('custom_id', None) + + values = {} + for row in component_data['components']: + for component in row['components']: + values[component['custom_id']] = component['value'] + self.values = values + + def _component_interaction_factory(data): component_type = data['data']['component_type'] diff --git a/bot/meta/interactions/modals.py b/bot/meta/interactions/modals.py new file mode 100644 index 00000000..56bcfd8e --- /dev/null +++ b/bot/meta/interactions/modals.py @@ -0,0 +1,54 @@ +import uuid + +from .enums import TextInputStyle, InteractionType +from .components import AwaitableComponent + + +class Modal(AwaitableComponent): + interaction_type = InteractionType.MODAL_SUBMIT + + def __init__(self, title, *components, custom_id=None): + self.custom_id = custom_id or str(uuid.uuid4()) + + self.title = title + self.components = components + + def to_dict(self): + data = { + 'title': self.title, + 'custom_id': self.custom_id, + 'components': [comp.to_dict() for comp in self.components] + } + return data + + +class TextInput: + _type = 4 + + def __init__( + self, + label, placeholder=None, value=None, required=False, + style=TextInputStyle.SHORT, min_length=None, max_length=None, + custom_id=None + ): + self.custom_id = custom_id or str(uuid.uuid4()) + + self.label = label + self.placeholder = placeholder + self.value = value + self.required = required + self.style = style + self.min_length = min_length + self.max_length = max_length + + def to_dict(self): + data = { + 'type': self._type, + 'custom_id': self.custom_id, + 'style': int(self.style), + 'label': self.label, + } + for key in ('min_length', 'max_length', 'required', 'value', 'placeholder'): + if (value := getattr(self, key)) is not None: + data[key] = value + return data diff --git a/bot/meta/patches.py b/bot/meta/patches.py index 14a5741e..f692c4da 100644 --- a/bot/meta/patches.py +++ b/bot/meta/patches.py @@ -3,19 +3,29 @@ Temporary patches for the discord.py library to support new features of the disc """ import logging +from json import JSONEncoder + from discord.state import ConnectionState from discord.http import Route, HTTPClient from discord.abc import Messageable -from discord.utils import InvalidArgument, _get_as_snowflake +from discord.utils import InvalidArgument, _get_as_snowflake, to_json from discord import File, AllowedMentions, Member, User, Message -from .interactions import _component_interaction_factory +from .interactions import _component_interaction_factory, ModalResponse from .interactions.enums import InteractionType log = logging.getLogger(__name__) +def _default(self, obj): + return getattr(obj.__class__, "to_json", _default.default)(obj) + + +_default.default = JSONEncoder().default +JSONEncoder.default = _default + + def send_message(self, channel_id, content, *, tts=False, embeds=None, nonce=None, allowed_mentions=None, message_reference=None, components=None): r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) @@ -39,12 +49,59 @@ def send_message(self, channel_id, content, *, tts=False, embeds=None, if message_reference: payload['message_reference'] = message_reference - if components is not None: + if components: payload['components'] = components return self.request(r, json=payload) +def send_files( + self, + channel_id, *, + files, + content=None, tts=False, embed=None, embeds=None, nonce=None, allowed_mentions=None, message_reference=None, + components=None +): + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) + form = [] + + payload = {'tts': tts} + if content: + payload['content'] = content + if embed: + payload['embed'] = embed + if embeds: + payload['embeds'] = embeds + if nonce: + payload['nonce'] = nonce + if allowed_mentions: + payload['allowed_mentions'] = allowed_mentions + if message_reference: + payload['message_reference'] = message_reference + if components: + payload['components'] = components + + form.append({'name': 'payload_json', 'value': to_json(payload)}) + if len(files) == 1: + file = files[0] + form.append({ + 'name': 'file', + 'value': file.fp, + 'filename': file.filename, + 'content_type': 'application/octet-stream' + }) + else: + for index, file in enumerate(files): + form.append({ + 'name': 'file%s' % index, + 'value': file.fp, + 'filename': file.filename, + 'content_type': 'application/octet-stream' + }) + + return self.request(r, form=form, files=files) + + def interaction_callback(self, interaction_id, interaction_token, callback_type, callback_data=None): r = Route( 'POST', @@ -62,7 +119,16 @@ def interaction_callback(self, interaction_id, interaction_token, callback_type, return self.request(r, json=payload) +def edit_message(self, channel_id, message_id, components=None, **fields): + r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id) + if components is not None: + fields['components'] = [comp.to_dict() for comp in components] + return self.request(r, json=fields) + + +HTTPClient.send_files = send_files HTTPClient.send_message = send_message +HTTPClient.edit_message = edit_message HTTPClient.interaction_callback = interaction_callback @@ -114,7 +180,7 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=N try: data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions, content=content, tts=tts, embed=embed, nonce=nonce, - message_reference=reference) + message_reference=reference, components=components) finally: file.close() @@ -126,8 +192,8 @@ async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=N try: data = await state.http.send_files(channel.id, files=files, content=content, tts=tts, - embed=embed, nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference) + embeds=embeds, nonce=nonce, allowed_mentions=allowed_mentions, + message_reference=reference, components=components) finally: for f in files: f.close() @@ -181,8 +247,11 @@ def parse_interaction_create(self, data): 'INTERACTION_CREATE recieved unhandled message component interaction type: %s', data['data']['component_type'] ) + elif data['type'] == InteractionType.MODAL_SUBMIT: + interaction = ModalResponse(message, user, data, self) else: log.debug('INTERACTION_CREATE recieved unhandled interaction type: %s', data['type']) + log.debug(data) interaction = None if interaction: From 413b54c5ab22b0d6ea0d8ef2a219fe371022d88f Mon Sep 17 00:00:00 2001 From: Conatum Date: Tue, 19 Apr 2022 13:46:44 +0300 Subject: [PATCH 5/5] (interactions): Support message response. --- bot/meta/interactions/components.py | 1 - bot/meta/interactions/interactions.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/bot/meta/interactions/components.py b/bot/meta/interactions/components.py index c7788546..81f21145 100644 --- a/bot/meta/interactions/components.py +++ b/bot/meta/interactions/components.py @@ -41,7 +41,6 @@ class AwaitableComponent: def _check(interaction): valid = True - print(interaction.custom_id) valid = valid and interaction.interaction_type == self.interaction_type valid = valid and interaction.custom_id == self.custom_id valid = valid and (check is None or check(interaction)) diff --git a/bot/meta/interactions/interactions.py b/bot/meta/interactions/interactions.py index f51df5ed..c1ec18e9 100644 --- a/bot/meta/interactions/interactions.py +++ b/bot/meta/interactions/interactions.py @@ -9,6 +9,27 @@ class Interaction: '_state' ) + async def response(self, content=None, embeds=None, components=None, ephemeral=None): + data = {} + if content is not None: + data['content'] = str(content) + + if embeds is not None: + data['embeds'] = [embed.to_dict() for embed in embeds] + + if components is not None: + data['components'] = [component.to_dict() for component in components] + + if ephemeral is not None: + data['flags'] = 1 << 6 + + return await self._state.http.interaction_callback( + self.id, + self.token, + InteractionCallback.CHANNEL_MESSAGE_WITH_SOURCE, + data + ) + async def response_deferred(self): return await self._state.http.interaction_callback( self.id,