Files
croccybot/src/modules/rooms/roomui.py

468 lines
17 KiB
Python

from typing import Optional, TYPE_CHECKING
import asyncio
import discord
from discord.ui.button import button, Button, ButtonStyle
from discord.ui.select import select, UserSelect
from meta import LionBot, conf
from meta.errors import UserInputError
from meta.logger import log_wrap
from babel.translator import ctx_locale
from utils.lib import utc_now, MessageArgs, error_embed
from utils.ui import MessageUI, input
from core.data import CoreData
from modules.pomodoro.ui import TimerOptionsUI, TimerEditor
from modules.pomodoro.lib import TimerRole
from modules.pomodoro.options import TimerOptions
from . import babel, logger
from .data import RoomData
from .settings import RoomSettings
if TYPE_CHECKING:
from .room import Room
_p = babel._p
class RoomUI(MessageUI):
"""
View status for and reconfigure a rented room.
May be used by both owners and members,
but members will get a simplified UI.
"""
def __init__(self, bot: LionBot, room: 'Room', **kwargs):
# Do we need to set the locale?
# The room never calls the UI itself, so we should always have context locale
# If this changes (e.g. persistent status), uncomment this.
# ctx_locale.set(room.lguild.config.get('guild_locale').value)
super().__init__(**kwargs)
self.bot = bot
self.room = room
async def owner_ward(self, interaction: discord.Interaction):
t = self.bot.translator.t
if not interaction.user.id == self.room.data.ownerid:
await interaction.response.send_message(
embed=discord.Embed(
colour=discord.Colour.brand_red(),
description=t(_p(
'ui:room_status|error:owner_required',
"You must be the private room owner to do this!"
))
),
ephemeral=True
)
return False
return True
async def interaction_check(self, interaction: discord.Interaction):
if interaction.user.id not in self.room.members and interaction.user.id != self.room.data.ownerid:
t = self.bot.translator.t
await interaction.response.send_message(
embed=discord.Embed(
colour=discord.Colour.brand_red(),
description=t(_p(
'ui:room_status|error:member_required',
"You need to be a member of the private room to do this!"
))
),
ephemeral=True
)
return False
return True
@button(label='DEPOSIT_PLACEHOLDER', style=ButtonStyle.green, emoji=conf.emojis.coin)
async def desposit_button(self, press: discord.Interaction, pressed: Button):
t = self.bot.translator.t
# Open modal, ask how much they want to deposit
try:
submit, response = await input(
press,
title=t(_p(
'ui:room_status|button:deposit|modal:deposit|title',
"Room Deposit"
)),
question=t(_p(
'ui:room_status|button:deposit|modal:deposit|field:question|label',
"How many LionCoins do you want to deposit?"
)),
required=True,
max_length=16
)
except asyncio.TimeoutError:
# Input timed out
# They probably just closed the dialogue
# Exit silently
return
# Input checking
response = response.strip()
if not response.isdigit() or (amount := int(response)) == 0:
await submit.response.send_message(
embed=error_embed(
t(_p(
'ui:room_status|button:deposit|error:invalid_number',
"Cannot deposit `{inputted}` coins. Please enter a positive integer."
)).format(inputted=response)
), ephemeral=True
)
return
await submit.response.defer(thinking=True, ephemeral=True)
await self._do_deposit(t, press, amount, submit)
# Post deposit message
await self.room.notify_deposit(press.user, amount)
await self.refresh(thinking=submit)
@log_wrap(isolate=True)
async def _do_deposit(self, t, press, amount, submit):
# Start transaction for deposit
async with self.bot.db.connection() as conn:
self.bot.db.conn = conn
async with conn.transaction():
# Get the lion balance directly
lion = await self.bot.core.data.Member.fetch(
self.room.data.guildid,
press.user.id,
cached=False
)
balance = lion.coins
if balance < amount:
await submit.edit_original_response(
embed=error_embed(
t(_p(
'ui:room_status|button:deposit|error:insufficient_funds',
"You cannot deposit {coin}**{amount}**! You only have {coin}**{balance}**."
)).format(
coin=self.bot.config.emojis.coin,
amount=amount,
balance=balance
)
)
)
return
# TODO: Economy Transaction
await lion.update(coins=CoreData.Member.coins - amount)
await self.room.data.update(coin_balance=RoomData.Room.coin_balance + amount)
async def desposit_button_refresh(self):
self.desposit_button.label = self.bot.translator.t(_p(
'ui:room_status|button:deposit|label',
"Deposit"
))
@button(label='EDIT_PLACEHOLDER', style=ButtonStyle.blurple)
async def edit_button(self, press: discord.Interaction, pressed: Button):
if not await self.owner_ward(press):
return
async def edit_button_refresh(self):
self.edit_button.label = self.bot.translator.t(_p(
'ui:room_status|button:edit|label',
"Edit Room"
))
self.edit_button.emoji = self.bot.config.emojis.config
@button(label='TIMER_PLACEHOLDER', style=ButtonStyle.green)
async def timer_button(self, press: discord.Interaction, pressed: Button):
if not await self.owner_ward(press):
return
timer = self.room.timer
if timer is not None:
await TimerEditor.open_editor(self.bot, press, timer, press.user)
else:
# Create a new owned timer
t = self.bot.translator.t
settings = [
TimerOptions.FocusLength,
TimerOptions.BreakLength,
TimerOptions.InactivityThreshold,
TimerOptions.BaseName,
TimerOptions.ChannelFormat
]
instances = [
setting(self.room.data.channelid, setting._default) for setting in settings
]
instances[3].data = self.room.data.name
inputs = [
instance.input_field for instance in instances
]
modal = TimerEditor(
*inputs,
title=t(_p(
'ui:room_status|button:timer|modal:add_timer|title',
"Create Room Timer"
))
)
@modal.submit_callback(timeout=10*60)
async def _create_timer_callback(submit: discord.Interaction):
try:
create_args = {
'channelid': self.room.data.channelid,
'guildid': self.room.data.guildid,
'ownerid': self.room.data.ownerid,
'notification_channelid': self.room.data.channelid,
'manager_roleid': press.guild.default_role.id,
}
for instance, field in zip(instances, inputs):
try:
parsed = await instance.from_string(self.room.data.channelid, field.value)
except UserInputError as e:
_msg = f"`{instance.display_name}:` {e._msg}"
raise UserInputError(_msg, info=e.info, details=e.details)
create_args[parsed._column] = parsed._data
# Parsing okay, start to create
await submit.response.defer(thinking=True)
timer_cog = self.bot.get_cog('TimerCog')
timer = await timer_cog.create_timer(**create_args)
await timer.start()
await submit.edit_original_response(
content=t(_p(
'ui:room_status|button:timer|timer_created',
"Timer created successfully! Use {edit_cmd} to configure further."
)).format(edit_cmd=self.bot.core.mention_cmd('pomodoro edit'))
)
await self.refresh()
except UserInputError:
raise
except Exception:
logger.exception(
"Unhandled exception occurred while creating timer for private room."
)
await press.response.send_modal(modal)
async def timer_button_refresh(self):
t = self.bot.translator.t
button = self.timer_button
if self.room.timer is not None:
button.label = t(_p(
'ui:room_status|button:timer|label:edit_timer',
"Edit Timer"
))
button.style = ButtonStyle.blurple
button.emoji = self.bot.config.emojis.config
else:
button.label = t(_p(
'ui:room_status|button:timer|label:add_timer',
"Add Timer"
))
button.style = ButtonStyle.green
button.emoji = self.bot.config.emojis.clock
@button(emoji=conf.emojis.refresh, style=ButtonStyle.grey)
async def refresh_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True, ephemeral=True)
await self.refresh(thinking=press)
async def refresh_button_refresh(self):
pass
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
async def close_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer()
await self.quit()
async def close_button_refresh(self):
pass
@select(cls=UserSelect, placeholder="INVITE_PLACEHOLDER", min_values=0, max_values=25)
async def invite_menu(self, selection: discord.Interaction, selected: UserSelect):
if not await self.owner_ward(selection):
return
t = self.bot.translator.t
userids = set(user.id for user in selected.values)
userids.discard(self.room.data.ownerid)
userids.difference_update(self.room.members)
if not userids:
# No new members were given, quietly exit
await selection.response.defer()
return
await selection.response.defer(thinking=True, ephemeral=True)
# Check cap
cap = self.room.lguild.config.get(RoomSettings.MemberLimit.setting_id).value
if len(self.room.members) + len(userids) >= cap:
await selection.edit_original_response(
embed=error_embed(
t(_p(
'ui:room_status|menu:invite|error:too_many_members',
"Too many members! "
"You are inviting `{count}` new members to your room, "
"but you already have `{current}` members! "
"The member cap is `{cap}`."
)).format(
count=len(userids),
current=len(self.room.members) + 1,
cap=cap
)
)
)
return
# Add the members
await self.room.add_new_members(userids)
await self.refresh(thinking=selection)
async def invite_menu_refresh(self):
t = self.bot.translator.t
menu = self.invite_menu
cap = self.room.lguild.config.get('rooms_slots').value
if len(self.room.members) >= cap:
menu.disabled = True
menu.placeholder = t(_p(
'ui:room_status|menu:invite_menu|placeholder:capped',
"Room member cap reached!"
))
else:
menu.disabled = False
menu.placeholder = t(_p(
'ui:room_status|menu:invite_menu|placeholder:notcapped',
"Add Members"
))
@select(cls=UserSelect, placeholder='KICK_MENU_PLACEHOLDER')
async def kick_menu(self, selection: discord.Interaction, selected: UserSelect):
if not await self.owner_ward(selection):
return
userids = set(user.id for user in selected.values)
userids.intersection_update(self.room.members)
if not userids:
# No selected users are actually members of the room
await selection.response.defer()
return
await selection.response.defer(thinking=True, ephemeral=True)
await self.room.rm_members(userids)
await self.refresh(thinking=selection)
async def kick_menu_refresh(self):
self.kick_menu.placeholder = self.bot.translator.t(_p(
'ui:room_status|menu:kick_menu|placeholder',
"Remove Members"
))
# ----- UI Flow -----
async def make_message(self) -> MessageArgs:
t = self.bot.translator.t
title = t(_p(
'ui:room_status|embed|title',
"Room Control Panel"
))
embed = discord.Embed(
title=title,
colour=discord.Colour.orange(),
)
embed.add_field(
name=t(_p('ui:room_status|embed|field:channel|name', "Channel")),
value=self.room.channel.mention
)
embed.add_field(
name=t(_p('ui:room_status|embed|field:owner|name', "Owner")),
value=f"<@{self.room.data.ownerid}>"
)
embed.add_field(
name=t(_p('ui:room_status|embed|field:created|name', "Created At")),
value=f"<t:{int(self.room.data.created_at.timestamp())}>"
)
balance = self.room.data.coin_balance
rent = self.room.rent
next_tick = f"<t:{int(self.room.next_tick.timestamp())}:R>"
if self.room.expiring:
bank_value = t(_p(
'ui:room_status|embed|field:bank|value:expiring',
"**Warning:** Insufficient room balance to pay next rent ({coin} **{rent}**).\n"
"The room will expire {expiry}.\nUse {room_deposit_cmd} to increase balance."
)).format(
coin=conf.emojis.coin,
amount=balance,
rent=rent,
expiry=next_tick,
room_deposit_cmd=self.bot.core.mention_cmd('room deposit')
)
else:
bank_value = t(_p(
'ui:room_status|embed|field:bank|value:notexpiring',
"Next rent due {time} (- {coin}**{rent}**)"
)).format(
coin=conf.emojis.coin,
amount=balance,
rent=rent,
time=next_tick
)
embed.add_field(
name=t(_p('ui:room_status|embed|field:bank|name', "Room Balance: {coin}**{amount}**")).format(
coin=self.bot.config.emojis.coin,
amount=balance
),
value=bank_value,
inline=False
)
member_cap = self.room.lguild.config.get('rooms_slots').value
embed.add_field(
name=t(_p(
'ui:room_status|embed|field:members|name',
"Members ({count}/{cap})"
)).format(count=len(self.room.members) + 1, cap=member_cap),
value=', '.join(f"<@{userid}>" for userid in (self.room.data.ownerid, *self.room.members))
)
return MessageArgs(embed=embed)
async def refresh_layout(self):
if self._callerid == self.room.data.ownerid:
# If the owner called, show full config UI
await asyncio.gather(
self.desposit_button_refresh(),
# self.edit_button_refresh(),
self.refresh_button_refresh(),
self.close_button_refresh(),
self.timer_button_refresh(),
self.invite_menu_refresh(),
self.kick_menu_refresh()
)
self.set_layout(
(self.desposit_button, self.timer_button, self.refresh_button, self.close_button),
(self.invite_menu, ),
(self.kick_menu, )
)
else:
# Just show deposit button
await asyncio.gather(
self.desposit_button_refresh(),
self.refresh_button_refresh(),
self.close_button_refresh(),
)
self.set_layout(
(self.desposit_button, self.refresh_button, self.close_button),
)
async def reload(self):
"""
No need to reload-data, as we use the room as source of truth.
"""
...