rewrite: Initial rewrite skeleton.

Remove modules that will no longer be required.
Move pending modules to pending-rewrite folders.
This commit is contained in:
2022-09-17 17:06:13 +10:00
parent a7f7dd6e7b
commit a5147323b5
162 changed files with 1 additions and 866 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "bot/cmdClient"]
path = bot/cmdClient
url = https://github.com/Intery/dpy-cmdClient.git

Submodule bot/cmdClient deleted from a6ece4cb02

View File

@@ -1,5 +0,0 @@
from . import enums
from .interactions import _component_interaction_factory, Interaction, ComponentInteraction, ModalResponse
from .components import *
from .modals import *
from .manager import InteractionManager

View File

@@ -1,190 +0,0 @@
import logging
import traceback
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
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 AwaitableComponent:
interaction_type: InteractionType = None
async def wait_for(self, timeout=None, check=None):
from meta import client
def _check(interaction):
valid = True
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
return await client.wait_for('interaction_create', timeout=timeout, check=_check)
def add_callback(self, timeout=None, repeat=True, check=None, pass_args=(), pass_kwargs={}):
def wrapper(func):
async def _func(*args, **kwargs):
try:
return await func(*args, **kwargs)
except asyncio.CancelledError:
pass
except asyncio.TimeoutError:
pass
except Exception:
from meta import client
full_traceback = traceback.format_exc()
client.log(
f"Caught an unhandled exception while executing interaction callback "
f"for interaction type '{self.interaction_type.name}' with id '{self.custom_id}'.\n"
f"{self!r}\n"
f"{func!r}\n"
f"{full_traceback}",
context=f"cid:{self.custom_id}",
level=logging.ERROR
)
async def wrapped():
while True:
try:
button_press = await self.wait_for(timeout=timeout, check=check)
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):
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 = str(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()
if self.disabled:
data['disabled'] = self.disabled
return data
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
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
return data
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
def set_default(self, value=None, index=None):
"""
Convenience method to set the default option.
"""
if index is not None and value is not None:
raise ValueError("Both index and value were supplied for the default.")
if index is not None:
for i, option in enumerate(self.options):
option.default = (i == index)
elif value is not None:
for option in self.options:
option.default = (option.value == value)
else:
for option in self.options:
option.default = False
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

View File

@@ -1,40 +0,0 @@
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 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

View File

@@ -1,168 +0,0 @@
import asyncio
from .enums import ComponentType, InteractionType, InteractionCallback
class Interaction:
__slots__ = (
'id',
'token',
'_state'
)
async def response(self, content=None, embed=None, embeds=None, components=None, ephemeral=None):
data = {}
if content is not None:
data['content'] = str(content)
if embed is not None:
embeds = embeds or []
embeds.append(embed)
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_update(self, content=None, embed=None, embeds=None, components=None):
data = {}
if content is not None:
data['content'] = str(content)
if embed is not None:
embeds = embeds or []
embeds.append(embed)
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]
return await self._state.http.interaction_callback(
self.id,
self.token,
InteractionCallback.UPDATE_MESSAGE,
data
)
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.response_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__ = ()
class Selection(ComponentInteraction):
__slots__ = ('values',)
def _from_data(self, data):
super()._from_data(data)
self.values = data['data']['values']
@property
def value(self):
if len(self.values) > 1:
raise ValueError("Cannot use 'value' property on multi-selection.")
elif len(self.values) == 1:
return self.values[0]
else:
return None
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']
if component_type == ComponentType.BUTTON:
return ButtonPress
elif component_type == ComponentType.SELECTMENU:
return Selection
else:
return None

View File

@@ -1,114 +0,0 @@
import asyncio
import logging
from datetime import datetime, timedelta
class InteractionManager:
def __init__(self, timeout=600, extend=None):
self.futures = []
self.self_futures = []
self.cleanup_function = self._cleanup
self.timeout_function = self._timeout
self.close_function = self._close
self.timeout = timeout
self.extend = extend or timeout
self.expires_at = None
self.cleaned_up = asyncio.Event()
async def _timeout_loop(self):
diff = (self.expires_at - datetime.now()).total_seconds()
while True:
try:
await asyncio.sleep(diff)
except asyncio.CancelledError:
break
diff = (self.expires_at - datetime.now()).total_seconds()
if diff <= 0:
asyncio.create_task(self.run_timeout())
break
def extend_timeout(self):
new_expiry = max(datetime.now() + timedelta(seconds=self.extend), self.expires_at)
self.expires_at = new_expiry
async def wait(self):
"""
Wait until the manager is "done".
That is, until all the futures are done, or `closed` is set.
"""
closed_task = asyncio.create_task(self.cleaned_up.wait())
futures_task = asyncio.create_task(asyncio.wait(self.futures))
await asyncio.wait((closed_task, futures_task), return_when=asyncio.FIRST_COMPLETED)
async def __aenter__(self):
if self.timeout is not None:
self.expires_at = datetime.now() + timedelta(seconds=self.timeout)
self.self_futures.append(
asyncio.create_task(self._timeout_loop())
)
return self
async def __aexit__(self, *args):
if not self.cleaned_up.is_set():
await self.cleanup(exiting=True)
async def _cleanup(self, manager, timeout=False, closing=False, exiting=False, **kwargs):
for future in self.futures:
future.cancel()
for future in self.self_futures:
future.cancel()
self.cleaned_up.set()
def on_cleanup(self, func):
self.cleanup_function = func
return func
async def cleanup(self, **kwargs):
try:
await self.cleanup_function(self, **kwargs)
except Exception:
logging.debug("An error occurred while cleaning up the InteractionManager", exc_info=True)
async def _timeout(self, manager, **kwargs):
await self.cleanup(timeout=True, **kwargs)
def on_timeout(self, func):
self.timeout_function = func
return func
async def run_timeout(self):
try:
await self.timeout_function(self)
except Exception:
logging.debug("An error occurred while timing out the InteractionManager", exc_info=True)
async def close(self, **kwargs):
"""
Request closure of the manager.
"""
try:
await self.close_function(self, **kwargs)
except Exception:
logging.debug("An error occurred while closing the InteractionManager", exc_info=True)
def on_close(self, func):
self.close_function = func
return func
async def _close(self, manager, **kwargs):
await self.cleanup(closing=True, **kwargs)
def add_future(self, future):
self.futures.append(future)
return future
def register(self, component, callback, *args, **kwargs):
"""
Attaches the given awaitable interaction and adds the given callback.
"""
future = component.add_callback(*args, **kwargs)(callback)
self.add_future(future)
return component

View File

@@ -1,54 +0,0 @@
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

View File

@@ -1,289 +0,0 @@
"""
Temporary patches for the discord.py library to support new features of the discord API.
"""
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, to_json
from discord import File, AllowedMentions, Member, User, Message
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)
payload = {}
if content:
payload['content'] = content
if tts:
payload['tts'] = True
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
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',
'/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)
def interaction_edit(self, application_id, interaction_token, content=None, embed=None, embeds=None, components=None):
r = Route(
'PATCH',
'/webhooks/{application_id}/{interaction_token}/messages/@original',
application_id=application_id,
interaction_token=interaction_token
)
payload = {}
if content is not None:
payload['content'] = str(content)
if embed is not None:
embeds = embeds or []
embeds.append(embed)
if embeds is not None:
payload['embeds'] = [embed.to_dict() for embed in embeds]
if components is not None:
payload['components'] = [component.to_dict() for component in components]
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
HTTPClient.interaction_edit = interaction_edit
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, components=None):
channel = await self._get_channel()
state = self._state
content = str(content) if content is not None else None
if embed is not None:
if embeds is not None:
embeds.append(embed)
else:
embeds = [embed]
embed = embed.to_dict()
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()
else:
allowed_mentions = allowed_mentions.to_dict()
else:
allowed_mentions = state.allowed_mentions and state.allowed_mentions.to_dict()
if mention_author is not None:
allowed_mentions = allowed_mentions or AllowedMentions().to_dict()
allowed_mentions['replied_user'] = bool(mention_author)
if reference is not None:
try:
reference = reference.to_message_reference_dict()
except AttributeError:
raise InvalidArgument('reference parameter must be Message or MessageReference') from None
if file is not None and files is not None:
raise InvalidArgument('cannot pass both file and files parameter to send()')
if file is not None:
if not isinstance(file, File):
raise InvalidArgument('file parameter must be File')
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, components=components)
finally:
file.close()
elif files is not None:
if len(files) > 10:
raise InvalidArgument('files parameter must be a list of up to 10 elements')
elif not all(isinstance(file, File) for file in files):
raise InvalidArgument('files parameter must be a list of File')
try:
data = await state.http.send_files(channel.id, files=files, content=content, tts=tts,
embeds=embeds, nonce=nonce, allowed_mentions=allowed_mentions,
message_reference=reference, components=components)
finally:
for f in files:
f.close()
else:
data = await state.http.send_message(channel.id, content, tts=tts, embeds=embeds,
nonce=nonce, allowed_mentions=allowed_mentions,
message_reference=reference, components=components)
ret = state.create_message(channel=channel, data=data)
if delete_after is not None:
await ret.delete(delay=delete_after)
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)
if 'message' in data:
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)
else:
message = None
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']
)
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:
self.dispatch('interaction_create', interaction)
ConnectionState.parse_interaction_create = parse_interaction_create

Some files were not shown because too many files have changed in this diff Show More