generated from HoloTech/discord-bot-template
1071 lines
35 KiB
Python
1071 lines
35 KiB
Python
from typing import Optional
|
|
import asyncio
|
|
import copy
|
|
import json
|
|
import datetime as dt
|
|
from io import StringIO
|
|
|
|
import discord
|
|
from discord.ui.button import button, Button, ButtonStyle
|
|
from discord.ui.select import select, Select, SelectOption
|
|
from discord.ui.text_input import TextInput, TextStyle
|
|
|
|
from meta import conf, LionBot
|
|
from meta.errors import UserInputError, ResponseTimedOut
|
|
|
|
from ..lib import MessageArgs, utc_now
|
|
|
|
from . import MessageUI, util_babel, error_handler_for, FastModal, ModalRetryUI, Confirm, AsComponents, AButton
|
|
|
|
|
|
_p = util_babel._p
|
|
|
|
|
|
class MsgEditorInput(FastModal):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
@error_handler_for(UserInputError)
|
|
async def rerequest(self, interaction, error):
|
|
await ModalRetryUI(self, error.msg).respond_to(interaction)
|
|
|
|
|
|
class MsgEditor(MessageUI):
|
|
def __init__(self, bot: LionBot, initial_data: dict, formatter=None, callback=None, **kwargs):
|
|
self.bot = bot
|
|
self.history = [initial_data] # Last item in history is current state
|
|
self.future = [] # Last item in future is next state
|
|
|
|
self._formatter = formatter
|
|
self._callback = callback
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
@property
|
|
def data(self):
|
|
return self.history[-1]
|
|
|
|
# ----- API -----
|
|
async def format_data(self, data):
|
|
"""
|
|
Format a MessageData dict for rendering.
|
|
|
|
May be extended or overridden for custom formatting.
|
|
By default, uses the provided `formatter` callback (if provided).
|
|
"""
|
|
if self._formatter is not None:
|
|
return await self._formatter(data)
|
|
else:
|
|
return data
|
|
|
|
|
|
def copy_data(self):
|
|
return copy.deepcopy(self.history[-1])
|
|
|
|
async def save(self):
|
|
...
|
|
|
|
async def push_change(self, new_data):
|
|
# Cleanup the data
|
|
if (embed_data := new_data.get('embed', None)) is not None and not embed_data:
|
|
new_data.pop('embed')
|
|
|
|
t = self.bot.translator.t
|
|
if 'embed' not in new_data and not new_data.get('content', None):
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|error:empty',
|
|
"Rendering failed! The message content and embed cannot both be empty."
|
|
))
|
|
)
|
|
|
|
if 'embed' in new_data:
|
|
try:
|
|
formatted_data = copy.deepcopy(new_data)
|
|
discord.Embed.from_dict(await self.format_data(formatted_data['embed']))
|
|
except Exception as e:
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|error:embed_failed',
|
|
"Rendering failed! Could not parse the embed.\n"
|
|
"Error: {error}"
|
|
)).format(error=str(e))
|
|
)
|
|
|
|
# Push the state and try displaying it
|
|
self.history.append(new_data)
|
|
old_future = self.future
|
|
self.future = []
|
|
try:
|
|
await self.refresh()
|
|
except discord.HTTPException as e:
|
|
# State failed, rollback and error
|
|
self.history.pop()
|
|
self.future = old_future
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|error:invalid_change',
|
|
"Rendering failed! The message was not modified.\n"
|
|
"Error: `{text}`"
|
|
)).format(text=e.text)
|
|
)
|
|
|
|
# ----- UI Components -----
|
|
|
|
# -- Content Only mode --
|
|
@button(label="EDIT_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def edit_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Open an editor for the message content
|
|
"""
|
|
data = self.copy_data()
|
|
|
|
t = self.bot.translator.t
|
|
content_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:content|field:content|label',
|
|
"Message Content"
|
|
)),
|
|
style=TextStyle.long,
|
|
required=False,
|
|
default=data.get('content', ""),
|
|
max_length=2000
|
|
)
|
|
modal = MsgEditorInput(
|
|
content_field,
|
|
title=t(_p('ui:msg_editor|modal:content|title', "Content Editor"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def content_modal_callback(interaction: discord.Interaction):
|
|
new_content = content_field.value
|
|
data['content'] = new_content
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def edit_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.edit_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:edit|label',
|
|
"Edit Content"
|
|
))
|
|
|
|
@button(label="ADD_EMBED_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def add_embed_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Attach an embed with some simple fields filled.
|
|
"""
|
|
await press.response.defer()
|
|
t = self.bot.translator.t
|
|
|
|
sample_embed = {
|
|
"title": t(_p('ui:msg_editor|button:add_embed|sample_embed|title', "Title Placeholder")),
|
|
"description": t(_p('ui:msg_editor|button:add_embed|sample_embed|description', "Description Placeholder")),
|
|
}
|
|
data = self.copy_data()
|
|
data['embed'] = sample_embed
|
|
await self.push_change(data)
|
|
|
|
async def add_embed_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.add_embed_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:add_embed|label',
|
|
"Add Embed"
|
|
))
|
|
|
|
@button(label="RM_EMBED_BUTTON_PLACEHOLDER", style=ButtonStyle.red)
|
|
async def rm_embed_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Remove the existing embed from the message.
|
|
"""
|
|
await press.response.defer()
|
|
t = self.bot.translator.t
|
|
data = self.copy_data()
|
|
data.pop('embed', None)
|
|
data.pop('embeds', None)
|
|
if not data.get('content', '').strip():
|
|
data['content'] = t(_p(
|
|
'ui:msg_editor|button:rm_embed|sample_content',
|
|
"Content Placeholder"
|
|
))
|
|
await self.push_change(data)
|
|
|
|
async def rm_embed_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.rm_embed_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:rm_embed|label',
|
|
"Remove Embed"
|
|
))
|
|
|
|
# -- Embed Mode --
|
|
|
|
@button(label="BODY_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def body_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Edit the Content, Description, Title, and Colour
|
|
"""
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
|
|
t = self.bot.translator.t
|
|
|
|
content_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:body|field:content|label',
|
|
"Message Content"
|
|
)),
|
|
style=TextStyle.long,
|
|
required=False,
|
|
default=data.get('content', ""),
|
|
max_length=2000
|
|
)
|
|
|
|
desc_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:body|field:desc|label',
|
|
"Embed Description"
|
|
)),
|
|
style=TextStyle.long,
|
|
required=False,
|
|
default=embed_data.get('description', ""),
|
|
max_length=4000
|
|
)
|
|
|
|
title_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:body|field:title|label',
|
|
"Embed Title"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=embed_data.get('title', ""),
|
|
max_length=256
|
|
)
|
|
|
|
colour_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:body|field:colour|label',
|
|
"Embed Colour"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=str(discord.Colour(value=embed_data['color'])) if 'color' in embed_data else '',
|
|
placeholder=str(discord.Colour.orange()),
|
|
max_length=7,
|
|
min_length=7
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
content_field,
|
|
title_field,
|
|
desc_field,
|
|
colour_field,
|
|
title=t(_p('ui:msg_editor|modal:body|title', "Message Body Editor"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def body_modal_callback(interaction: discord.Interaction):
|
|
data['content'] = content_field.value
|
|
|
|
if desc_field.value:
|
|
embed_data['description'] = desc_field.value
|
|
else:
|
|
embed_data.pop('description', None)
|
|
|
|
if title_field.value:
|
|
embed_data['title'] = title_field.value
|
|
else:
|
|
embed_data.pop('title', None)
|
|
|
|
if colour_field.value:
|
|
colourstr = colour_field.value
|
|
try:
|
|
colour = discord.Colour.from_str(colourstr)
|
|
except ValueError:
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|button:body|error:invalid_colour',
|
|
"Invalid colour format! Please enter colours as hex codes, e.g. `#E67E22`"
|
|
))
|
|
)
|
|
embed_data['color'] = colour.value
|
|
else:
|
|
embed_data.pop('color', None)
|
|
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def body_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.body_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:body|label',
|
|
"Body"
|
|
))
|
|
|
|
@button(label="AUTHOR_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def author_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Edit the embed author (author name/link/image url)
|
|
"""
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
author_data = embed_data.get('author', {})
|
|
|
|
t = self.bot.translator.t
|
|
|
|
name_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:author|field:name|label',
|
|
"Author Name"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=author_data.get('name', ''),
|
|
max_length=256
|
|
)
|
|
|
|
link_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:author|field:link|label',
|
|
"Author URL"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=author_data.get('url', ''),
|
|
)
|
|
|
|
image_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:author|field:image|label',
|
|
"Author Image URL"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=author_data.get('icon_url', ''),
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
name_field,
|
|
link_field,
|
|
image_field,
|
|
title=t(_p('ui:msg_editor|modal:author|title', "Embed Author Editor"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def author_modal_callback(interaction: discord.Interaction):
|
|
if (name := name_field.value):
|
|
author_data['name'] = name
|
|
author_data['icon_url'] = image_field.value
|
|
author_data['url'] = link_field.value
|
|
embed_data['author'] = author_data
|
|
else:
|
|
embed_data.pop('author', None)
|
|
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def author_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.author_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:author|label',
|
|
"Author"
|
|
))
|
|
|
|
@button(label="FOOTER_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def footer_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Open the Footer editor (edit footer icon, text, timestamp).
|
|
"""
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
footer_data = embed_data.get('footer', {})
|
|
|
|
t = self.bot.translator.t
|
|
|
|
text_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:footer|field:text|label',
|
|
"Footer Text"
|
|
)),
|
|
style=TextStyle.long,
|
|
required=False,
|
|
default=footer_data.get('text', ''),
|
|
max_length=2048
|
|
)
|
|
|
|
image_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:footer|field:image|label',
|
|
"Footer Image URL"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=footer_data.get('icon_url', ''),
|
|
)
|
|
|
|
timestamp_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:footer|field:timestamp|label',
|
|
"Embed Timestamp (in ISO format)"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=embed_data.get('timestamp', ''),
|
|
placeholder=utc_now().replace(microsecond=0).isoformat(sep=' ')
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
text_field,
|
|
image_field,
|
|
timestamp_field,
|
|
title=t(_p('ui:msg_editor|modal:footer|title', "Embed Footer Editor"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def footer_modal_callback(interaction: discord.Interaction):
|
|
if (text := text_field.value):
|
|
footer_data['text'] = text
|
|
footer_data['icon_url'] = image_field.value
|
|
embed_data['footer'] = footer_data
|
|
else:
|
|
embed_data.pop('footer', None)
|
|
|
|
if (ts := timestamp_field.value):
|
|
if ts.isdigit():
|
|
# Treat as UTC timestamp
|
|
timestamp = dt.datetime.fromtimestamp(int(ts), dt.timezone.utc)
|
|
ts = timestamp.isoformat()
|
|
to_validate = ts
|
|
elif self._formatter:
|
|
to_validate = await self._formatter(ts)
|
|
else:
|
|
to_validate = ts
|
|
try:
|
|
dt.datetime.fromisoformat(to_validate)
|
|
except ValueError:
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|button:footer|error:invalid_timestamp',
|
|
"Invalid timestamp! Please enter the timestamp in ISO format."
|
|
))
|
|
)
|
|
embed_data['timestamp'] = ts
|
|
else:
|
|
embed_data.pop('timestamp', None)
|
|
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def footer_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.footer_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:footer|label',
|
|
"Footer"
|
|
))
|
|
|
|
@button(label="IMAGES_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def images_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Edit the embed images (thumbnail and main image).
|
|
"""
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
thumb_data = embed_data.get('thumbnail', {})
|
|
image_data = embed_data.get('image', {})
|
|
|
|
t = self.bot.translator.t
|
|
|
|
thumb_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:images|field:thumb|label',
|
|
"Thumbnail Image URL"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=thumb_data.get('url', ''),
|
|
)
|
|
|
|
image_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:images|field:image|label',
|
|
"Embed Image URL"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
default=image_data.get('url', ''),
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
thumb_field,
|
|
image_field,
|
|
title=t(_p('ui:msg_editor|modal:images|title', "Embed images Editor"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def images_modal_callback(interaction: discord.Interaction):
|
|
if (thumb_url := thumb_field.value):
|
|
thumb_data['url'] = thumb_url
|
|
embed_data['thumbnail'] = thumb_data
|
|
else:
|
|
embed_data.pop('thumbnail', None)
|
|
|
|
if (image_url := image_field.value):
|
|
image_data['url'] = image_url
|
|
embed_data['image'] = image_data
|
|
else:
|
|
embed_data.pop('image', None)
|
|
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def images_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.images_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:images|label',
|
|
"Images"
|
|
))
|
|
|
|
@button(label="ADD_FIELD_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def add_field_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Add an embed field (position, name, value, inline)
|
|
"""
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
orig_fields = field_data.copy()
|
|
|
|
t = self.bot.translator.t
|
|
|
|
position_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:add_field|field:position|label',
|
|
"Field number to insert at"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=True,
|
|
default=str(len(field_data)),
|
|
)
|
|
|
|
name_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:add_field|field:name|label',
|
|
"Field name"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=False,
|
|
max_length=256,
|
|
)
|
|
|
|
value_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:add_field|field:value|label',
|
|
"Field value"
|
|
)),
|
|
style=TextStyle.long,
|
|
required=True,
|
|
max_length=1024,
|
|
)
|
|
|
|
inline_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:add_field|field:inline|label',
|
|
"Whether the field is inline"
|
|
)),
|
|
placeholder=t(_p(
|
|
'ui:msg_editor|modal:add_field|field:inline|placeholder',
|
|
"True/False"
|
|
)),
|
|
style=TextStyle.short,
|
|
required=True,
|
|
max_length=256,
|
|
default='True',
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
name_field,
|
|
value_field,
|
|
position_field,
|
|
inline_field,
|
|
title=t(_p('ui:msg_editor|modal:add_field|title', "Add Embed Field"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def add_field_modal_callback(interaction: discord.Interaction):
|
|
if inline_field.value.lower() == 'true':
|
|
inline = True
|
|
else:
|
|
inline = False
|
|
field = {
|
|
'name': name_field.value,
|
|
'value': value_field.value,
|
|
'inline': inline
|
|
}
|
|
try:
|
|
position = int(position_field.value)
|
|
except ValueError:
|
|
raise UserInputError(
|
|
t(_p(
|
|
'ui:msg_editor|modal:add_field|error:position_not_int',
|
|
"The field position must be an integer!"
|
|
))
|
|
)
|
|
field_data = orig_fields.copy()
|
|
field_data.insert(position, field)
|
|
embed_data['fields'] = field_data
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await press.response.send_modal(modal)
|
|
|
|
async def add_field_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.add_field_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:add_field|label',
|
|
"Add Field"
|
|
))
|
|
data = self.history[-1]
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
button.disabled = (len(field_data) >= 25)
|
|
|
|
def _field_option(self, index, field_data):
|
|
t = self.bot.translator.t
|
|
|
|
name = field_data.get('name', "")
|
|
value = field_data['value']
|
|
|
|
if not name:
|
|
name = t(_p(
|
|
'ui:msg_editor|format_field|name_placeholder',
|
|
"-"
|
|
))
|
|
|
|
name = f"{index+1}. {name}"
|
|
if len(name) > 100:
|
|
name = name[:97] + '...'
|
|
|
|
if len(value) > 100:
|
|
value = value[:97] + '...'
|
|
|
|
return SelectOption(label=name, description=value, value=str(index))
|
|
|
|
@select(cls=Select, placeholder="EDIT_FIELD_MENU_PLACEHOLDER", max_values=1)
|
|
async def edit_field_menu(self, selection: discord.Interaction, selected: Select):
|
|
if not selected.values:
|
|
await selection.response.defer()
|
|
return
|
|
|
|
index = int(selected.values[0])
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
field = field_data[index]
|
|
|
|
t = self.bot.translator.t
|
|
|
|
name_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:edit_field|field:name|label',
|
|
"Field name"
|
|
)),
|
|
style=TextStyle.short,
|
|
default=field.get('name', ''),
|
|
required=False,
|
|
max_length=256,
|
|
)
|
|
|
|
value_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:edit_field|field:value|label',
|
|
"Field value"
|
|
)),
|
|
style=TextStyle.long,
|
|
default=field.get('value', ''),
|
|
required=True,
|
|
max_length=1024,
|
|
)
|
|
|
|
inline_field = TextInput(
|
|
label=t(_p(
|
|
'ui:msg_editor|modal:edit_field|field:inline|label',
|
|
"Whether the field is inline"
|
|
)),
|
|
placeholder=t(_p(
|
|
'ui:msg_editor|modal:edit_field|field:inline|placeholder',
|
|
"True/False"
|
|
)),
|
|
default='True' if field.get('inline', True) else 'False',
|
|
style=TextStyle.short,
|
|
required=True,
|
|
max_length=256,
|
|
)
|
|
|
|
modal = MsgEditorInput(
|
|
name_field,
|
|
value_field,
|
|
inline_field,
|
|
title=t(_p('ui:msg_editor|modal:edit_field|title', "Edit Embed Field"))
|
|
)
|
|
|
|
@modal.submit_callback()
|
|
async def edit_field_modal_callback(interaction: discord.Interaction):
|
|
if inline_field.value.lower() == 'true':
|
|
inline = True
|
|
else:
|
|
inline = False
|
|
field = {
|
|
'name': name_field.value,
|
|
'value': value_field.value,
|
|
'inline': inline
|
|
}
|
|
field_data[index] = field
|
|
embed_data['fields'] = field_data
|
|
data['embed'] = embed_data
|
|
|
|
await self.push_change(data)
|
|
|
|
await interaction.response.defer()
|
|
|
|
await selection.response.send_modal(modal)
|
|
|
|
async def edit_field_menu_refresh(self):
|
|
t = self.bot.translator.t
|
|
menu = self.edit_field_menu
|
|
menu.placeholder = t(_p(
|
|
'ui:msg_editor|menu:edit_field|placeholder',
|
|
"Edit Embed Field"
|
|
))
|
|
data = self.history[-1]
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
|
|
if len(field_data) == 0:
|
|
menu.disabled = True
|
|
menu.options = [
|
|
SelectOption(label='Dummy')
|
|
]
|
|
else:
|
|
menu.disabled = False
|
|
menu.options = [
|
|
self._field_option(i, field)
|
|
for i, field in enumerate(field_data)
|
|
]
|
|
|
|
@select(cls=Select, placeholder="DELETE_FIELD_MENU_PLACEHOLDER", max_values=1)
|
|
async def delete_field_menu(self, selection: discord.Interaction, selected: Select):
|
|
if not selected.values:
|
|
await selection.response.defer()
|
|
return
|
|
|
|
index = int(selected.values[0])
|
|
data = self.copy_data()
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
field_data.pop(index)
|
|
if not field_data:
|
|
embed_data.pop('fields')
|
|
await self.push_change(data)
|
|
await selection.response.defer()
|
|
|
|
async def delete_field_menu_refresh(self):
|
|
t = self.bot.translator.t
|
|
menu = self.delete_field_menu
|
|
menu.placeholder = t(_p(
|
|
'ui:msg_deleteor|menu:delete_field|placeholder',
|
|
"Remove Embed Field"
|
|
))
|
|
data = self.history[-1]
|
|
embed_data = data.get('embed', {})
|
|
field_data = embed_data.get('fields', [])
|
|
|
|
if len(field_data) == 0:
|
|
menu.disabled = True
|
|
menu.options = [
|
|
SelectOption(label='Dummy')
|
|
]
|
|
else:
|
|
menu.disabled = False
|
|
menu.options = [
|
|
self._field_option(i, field)
|
|
for i, field in enumerate(field_data)
|
|
]
|
|
|
|
# -- Shared --
|
|
@button(label="SAVE_BUTTON_PLACEHOLDER", style=ButtonStyle.green)
|
|
async def save_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Saving simply resets the undo stack and calls the callback function.
|
|
Presumably the callback is hooked up to data or similar.
|
|
"""
|
|
await press.response.defer(thinking=True, ephemeral=True)
|
|
if self._callback is not None:
|
|
await self._callback(self.data)
|
|
self.history = self.history[-1:]
|
|
await self.refresh(thinking=press)
|
|
|
|
async def save_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.save_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:save|label',
|
|
"Save"
|
|
))
|
|
if len(self.history) > 1:
|
|
original = json.dumps(self.history[0])
|
|
current = json.dumps(self.history[-1])
|
|
button.disabled = (original == current)
|
|
else:
|
|
button.disabled = True
|
|
|
|
@button(label="DOWNLOAD_BUTTON_PLACEHOLDER", style=ButtonStyle.grey)
|
|
async def download_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Reply ephemerally with a formatted json version of the message content.
|
|
"""
|
|
data = json.dumps(self.history[-1], indent=2)
|
|
with StringIO(data) as fp:
|
|
fp.seek(0)
|
|
file = discord.File(fp, filename='message.json')
|
|
await press.response.send_message(file=file, ephemeral=True)
|
|
|
|
async def download_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.download_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:download|label',
|
|
"Download"
|
|
))
|
|
|
|
@button(label="UNDO_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def undo_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Pop the history stack.
|
|
"""
|
|
if len(self.history) > 1:
|
|
state = self.history.pop()
|
|
self.future.append(state)
|
|
await press.response.defer()
|
|
await self.refresh()
|
|
|
|
async def undo_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.undo_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:undo|label',
|
|
"Undo"
|
|
))
|
|
button.disabled = (len(self.history) <= 1)
|
|
|
|
@button(label="REDO_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
async def redo_button(self, press: discord.Interaction, pressed: Button):
|
|
"""
|
|
Pop the future stack.
|
|
"""
|
|
if len(self.future) > 0:
|
|
state = self.future.pop()
|
|
self.history.append(state)
|
|
await press.response.defer()
|
|
await self.refresh()
|
|
|
|
async def redo_button_refresh(self):
|
|
t = self.bot.translator.t
|
|
button = self.redo_button
|
|
button.label = t(_p(
|
|
'ui:msg_editor|button:redo|label',
|
|
"Redo"
|
|
))
|
|
button.disabled = (len(self.future) == 0)
|
|
|
|
@button(style=ButtonStyle.grey, emoji=conf.emojis.cancel)
|
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
|
# Confirm quit if there are unsaved changes
|
|
unsaved = False
|
|
if len(self.history) > 1:
|
|
original = json.dumps(self.history[0])
|
|
current = json.dumps(self.history[-1])
|
|
if original != current:
|
|
unsaved = True
|
|
|
|
# Confirmation prompt
|
|
if unsaved:
|
|
t = self.bot.translator.t
|
|
confirm_msg = t(_p(
|
|
'ui:msg_editor|button:quit|confirm',
|
|
"You have unsaved changes! Are you sure you want to quit?"
|
|
))
|
|
confirm = Confirm(confirm_msg, self._callerid)
|
|
confirm.confirm_button.label = t(_p(
|
|
'ui:msg_editor|button:quit|confirm|button:yes',
|
|
"Yes, Quit Now"
|
|
))
|
|
confirm.confirm_button.style = ButtonStyle.red
|
|
confirm.cancel_button.style = ButtonStyle.green
|
|
confirm.cancel_button.label = t(_p(
|
|
'ui:msg_editor|button:quit|confirm|button:no',
|
|
"No, Go Back"
|
|
))
|
|
try:
|
|
result = await confirm.ask(press, ephemeral=True)
|
|
except ResponseTimedOut:
|
|
result = False
|
|
|
|
if result:
|
|
await self.quit()
|
|
else:
|
|
await self.quit()
|
|
|
|
# ----- UI Flow -----
|
|
async def make_message(self) -> MessageArgs:
|
|
data = self.copy_data()
|
|
await self.format_data(data)
|
|
|
|
args = {}
|
|
args['content'] = data.get('content', '')
|
|
|
|
if 'embed' in data:
|
|
args['embed'] = discord.Embed.from_dict(data['embed'])
|
|
else:
|
|
args['embed'] = None
|
|
|
|
return MessageArgs(**args)
|
|
|
|
async def refresh_layout(self):
|
|
to_refresh = (
|
|
self.edit_button_refresh(),
|
|
self.add_embed_button_refresh(),
|
|
self.body_button_refresh(),
|
|
self.author_button_refresh(),
|
|
self.footer_button_refresh(),
|
|
self.images_button_refresh(),
|
|
self.add_field_button_refresh(),
|
|
self.edit_field_menu_refresh(),
|
|
self.delete_field_menu_refresh(),
|
|
self.save_button_refresh(),
|
|
self.download_button_refresh(),
|
|
self.undo_button_refresh(),
|
|
self.redo_button_refresh(),
|
|
self.rm_embed_button_refresh(),
|
|
)
|
|
await asyncio.gather(*to_refresh)
|
|
|
|
if self.history[-1].get('embed', None):
|
|
self.set_layout(
|
|
(self.body_button, self.author_button, self.footer_button, self.images_button, self.add_field_button),
|
|
(self.edit_field_menu,),
|
|
(self.delete_field_menu,),
|
|
(self.rm_embed_button,),
|
|
(self.save_button, self.download_button, self.undo_button, self.redo_button, self.quit_button),
|
|
)
|
|
else:
|
|
self.set_layout(
|
|
(self.edit_button, self.add_embed_button),
|
|
(self.save_button, self.download_button, self.undo_button, self.redo_button, self.quit_button),
|
|
)
|
|
|
|
async def reload(self):
|
|
# All data is handled by components, so nothing to do here
|
|
pass
|
|
|
|
async def redraw(self, thinking: Optional[discord.Interaction] = None):
|
|
"""
|
|
Overriding MessageUI.redraw to propagate exception.
|
|
"""
|
|
await self.refresh_layout()
|
|
args = await self.make_message()
|
|
|
|
if thinking is not None and not thinking.is_expired() and thinking.response.is_done():
|
|
asyncio.create_task(thinking.delete_original_response())
|
|
|
|
if self._original and not self._original.is_expired():
|
|
await self._original.edit_original_response(**args.edit_args, view=self)
|
|
elif self._message:
|
|
await self._message.edit(**args.edit_args, view=self)
|
|
else:
|
|
# Interaction expired or already closed. Quietly cleanup.
|
|
await self.close()
|
|
|
|
async def pre_timeout(self):
|
|
unsaved = False
|
|
if len(self.history) > 1:
|
|
original = json.dumps(self.history[0])
|
|
current = json.dumps(self.history[-1])
|
|
if original != current:
|
|
unsaved = True
|
|
|
|
# Timeout confirmation
|
|
if unsaved:
|
|
t = self.bot.translator.t
|
|
grace_period = 60
|
|
grace_time = utc_now() + dt.timedelta(seconds=grace_period)
|
|
embed = discord.Embed(
|
|
title=t(_p(
|
|
'ui:msg_editor|timeout_warning|title',
|
|
"Warning!"
|
|
)),
|
|
description=t(_p(
|
|
'ui:msg_editor|timeout_warning|desc',
|
|
"This interface will time out {timestamp}. Press 'Continue' below to keep editing."
|
|
)).format(
|
|
timestamp=discord.utils.format_dt(grace_time, style='R')
|
|
),
|
|
)
|
|
|
|
components = None
|
|
stopped = False
|
|
|
|
@AButton(label=t(_p('ui:msg_editor|timeout_warning|continue', "Continue")), style=ButtonStyle.green)
|
|
async def cont_button(interaction: discord.Interaction, pressed):
|
|
await interaction.response.defer()
|
|
await interaction.message.delete()
|
|
nonlocal stopped
|
|
stopped = True
|
|
# TODO: Clean up this mess. It works, but needs to be refactored to a timeout confirmation mixin.
|
|
# TODO: Consider moving the message to the interaction response
|
|
self._refresh_timeout()
|
|
components.stop()
|
|
|
|
components = AsComponents(cont_button, timeout=grace_period)
|
|
message = await self._original.channel.send(content=f"<@{self._callerid}>", embed=embed, view=components)
|
|
await components.wait()
|
|
|
|
if not stopped:
|
|
try:
|
|
await message.delete()
|
|
except discord.HTTPException:
|
|
pass
|