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

1061 lines
40 KiB
Python

from typing import Optional
from collections import defaultdict
import asyncio
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
from discord.app_commands import Range
from meta import LionCog, LionBot, LionContext
from meta.logger import log_wrap
from meta.errors import ResponseTimedOut
from meta.sharding import THIS_SHARD
from utils.lib import utc_now, error_embed
from utils.ui import Confirm
from constants import MAX_COINS
from core.data import CoreData
from wards import high_management_ward
from . import babel, logger
from .data import RoomData
from .settings import RoomSettings
from .settingui import RoomSettingUI
from .room import Room
from .roomui import RoomUI
from .lib import parse_members, owner_overwrite, member_overwrite, bot_overwrite
_p, _np = babel._p, babel._np
class RoomCog(LionCog):
def __init__(self, bot: LionBot):
self.bot = bot
self.data = bot.db.load_registry(RoomData())
self.settings = RoomSettings()
self.ready = False
self.event_lock = asyncio.Lock()
self._room_cache = defaultdict(dict) # Map guildid -> channelid -> Room
self._ticker_tasks = {} # Map channelid -> room run task
async def cog_load(self):
await self.data.init()
for setting in self.settings.model_settings:
self.bot.core.guild_config.register_model_setting(setting)
configcog = self.bot.get_cog('ConfigCog')
self.crossload_group(self.configure_group, configcog.admin_config_group)
if self.bot.is_ready():
await self.initialise()
async def cog_unload(self):
# Cancel room tick loops
for task in self._ticker_tasks.values():
task.cancel()
def get_rooms(self, guildid: int, userid: Optional[int] = None):
"""
Get the private rooms in the given guild, using cache.
If `userid` is provided, filters by rooms which the given user is a member or owner of.
"""
guild_rooms = self._room_cache[guildid]
if userid:
rooms = {
cid: room for cid, room in guild_rooms.items() if userid in room.members or userid == room.data.ownerid
}
else:
rooms = guild_rooms
return rooms
async def _prepare_rooms(self, room_data: list[RoomData.Room]):
"""
Launch or destroy rooms from the provided room data.
Client cache MUST be initialised, or rooms will be destroyed.
"""
# Launch or destroy rooms for the given data rows
to_delete = []
to_launch = []
lguildids = set()
for row in room_data:
channel = self.bot.get_channel(row.channelid)
if channel is None:
to_delete.append(row.channelid)
else:
lguildids.add(row.guildid)
to_launch.append(row)
if to_delete:
now = utc_now()
await self.data.Room.table.update_where(channelid=to_delete).set(deleted_at=now)
room_list = ', '.join(map(str, to_delete))
logger.info(
f"Deleted {len(to_delete)} private rooms with no underlying channel: {room_list}"
)
if to_launch:
lguilds = await self.bot.core.lions.fetch_guilds(*lguildids)
member_data = await self.data.RoomMember.fetch_where(channelid=[r.channelid for r in to_launch])
member_map = defaultdict(list)
for row in member_data:
member_map[row.channelid].append(row.userid)
for row in to_launch:
room = Room(self.bot, row, lguilds[row.guildid], member_map[row.channelid])
self._start(room)
logger.info(
f"Launched ticker tasks for {len(to_launch)} private rooms."
)
def _start(self, room: Room):
task = asyncio.create_task(self._ticker(room))
key = room.data.channelid
self._ticker_tasks[key] = task
task.add_done_callback(lambda fut: self._ticker_tasks.pop(key, None))
async def _ticker(self, room: Room):
cache = self._room_cache
cache[room.data.guildid][room.data.channelid] = room
try:
await room.run()
except asyncio.CancelledError:
pass
except Exception:
logger.exception(
f"Unhandled exception during room run task. This should not happen! {room.data!r}"
)
finally:
cache[room.data.guildid].pop(room.data.channelid)
# ----- Event Handlers -----
@LionCog.listener('on_ready')
@log_wrap(action='Init Rooms')
async def initialise(self):
"""
Restore rented channels.
"""
async with self.event_lock:
# Cancel any running tickers, we will recreate them
for task in self._ticker_tasks.values():
task.cancel()
room_data = await self.data.Room.fetch_where(THIS_SHARD, deleted_at=None)
await self._prepare_rooms(room_data)
logger.info(
f"Private Room system initialised with {len(self._ticker_tasks)} running rooms."
)
@LionCog.listener('on_guild_remove')
@log_wrap(action='Destroy Guild Rooms')
async def _unload_guild_rooms(self, guild: discord.Guild):
if guild.id in self._room_cache:
rooms = list(self._room_cache[guild.id].values())
for room in rooms:
await room.destroy("Guild Removed")
logger.info(
f"Deleted {len(rooms)} private rooms after leaving guild."
)
# Channel delete event handler
@LionCog.listener('on_guild_channel_delete')
@log_wrap(action='Destroy Channel Room')
async def _destroy_channel_room(self, channel: discord.abc.GuildChannel):
room = self._room_cache[channel.guild.id].get(channel.id, None)
if room is not None:
t = self.bot.translator.t
room.lguild.log_event(
title=t(_p(
'room|eventlog|event:room_deleted|title',
"Private Room Deleted"
)),
description=t(_p(
'room|eventlog|event:room_deleted|desc',
"{owner}'s private room was deleted."
)).format(
owner="<@{mid}>".format(mid=room.data.ownerid),
),
fields=room.eventlog_fields()
)
await room.destroy(reason="Underlying Channel Deleted")
# Setting event handlers
@LionCog.listener('on_guildset_rooms_category')
@log_wrap(action='Update Rooms Category')
async def _update_rooms_category(self, guildid: int, setting: RoomSettings.Category):
"""
Move all active private channels to the new category.
This shouldn't affect the channel function at all.
"""
data = setting.data
guild = self.bot.get_guild(guildid)
new_category = guild.get_channel(data) if guild and data else None
if new_category:
tasks = []
for room in list(self._room_cache[guildid].values()):
if (channel := room.channel) is not None and channel.category != new_category:
tasks.append(channel.edit(category=new_category))
if tasks:
try:
await asyncio.gather(*tasks)
except Exception:
logger.exception(
"Unhandled exception updating private room category."
)
@LionCog.listener('on_guildset_rooms_visible')
@log_wrap(action='Update Rooms Visibility')
async def _update_rooms_visibility(self, guildid: int, setting: RoomSettings.Visible):
"""
Update the everyone override on each room to reflect the new setting.
"""
data = setting.data
tasks = []
for room in list(self._room_cache[guildid].values()):
if room.channel:
tasks.append(
room.channel.set_permissions(
room.channel.guild.default_role,
view_channel=data
)
)
if tasks:
try:
await asyncio.gather(*tasks)
except Exception:
logger.exception(
"Unhandled exception updating private room visibility!"
)
# ----- Room API -----
@log_wrap(action="Create Room")
async def create_private_room(self,
guild: discord.Guild, owner: discord.Member,
initial_balance: int, name: str, members: list[discord.Member]
) -> Room:
"""
Create a new private room.
"""
t = self.bot.translator.t
lguild = await self.bot.core.lions.fetch_guild(guild.id)
# TODO: Consider extending invites to members rather than giving them immediate access
# Potential for abuse in moderation-free channel a member can add anyone too
everyone_overwrite = discord.PermissionOverwrite(
view_channel=lguild.config.get(RoomSettings.Visible.setting_id).value,
connect=False
)
# Build permission overwrites for owner and members, take into account visible setting
overwrites = {
owner: owner_overwrite,
guild.default_role: everyone_overwrite,
guild.me: bot_overwrite,
}
for member in members:
overwrites[member] = member_overwrite
# Create channel
try:
channel = await guild.create_voice_channel(
name=name,
reason=t(_p(
'create_room|create_channel|audit_reason',
"Creating Private Room for {ownerid}"
)).format(ownerid=owner.id),
category=lguild.config.get(RoomSettings.Category.setting_id).value,
overwrites=overwrites
)
except discord.HTTPException as e:
lguild.log_event(
t(_p(
'eventlog|event:private_room_create_error|name',
"Private Room Creation Failed"
)),
t(_p(
'eventlog|event:private_room_create_error|desc',
"{owner} attempted to rent a new private room, but I could not create it!\n"
"They were not charged."
)).format(owner=owner.mention),
errors=[f"`{repr(e)}`"]
)
raise
try:
# Create Room
now = utc_now()
data = await self.data.Room.create(
channelid=channel.id,
guildid=guild.id,
ownerid=owner.id,
coin_balance=initial_balance,
name=name,
created_at=now,
last_tick=now
)
if members:
await self.data.RoomMember.table.insert_many(
('channelid', 'userid'),
*((channel.id, member.id) for member in members)
)
room = Room(
self.bot,
data,
lguild,
[member.id for member in members]
)
except Exception:
try:
await channel.delete(reason="Failed to created private room")
except discord.HTTPException:
pass
logger.exception(
"Unhandled exception occurred while trying to create a new private room!"
)
raise
else:
logger.info(
f"New private room created: {room.data!r}"
)
lguild.log_event(
t(_p(
'eventlog|event:private_room_create|name',
"Private Room Rented"
)),
t(_p(
'eventlog|event:private_room_create|desc',
"{owner} has rented a new private room {channel}!"
)).format(owner=owner.mention, channel=channel.mention),
fields=room.eventlog_fields(),
)
return room
async def destroy_private_room(self, room: Room, reason: Optional[str] = None):
"""
Delete a private room.
Since this destroys the room, it will automatically remove itself from the running cache.
"""
await room.destroy(reason=reason)
def get_channel_room(self, channelid: int) -> Optional[Room]:
"""
Get a private room if it exists in the given channel.
"""
channel = self.bot.get_channel(channelid)
if channel:
room = self._room_cache[channel.guild.id].get(channelid, None)
return room
def get_owned_room(self, guildid: int, userid: int) -> Optional[Room]:
"""
Get a private room owned by the given member, if it exists.
"""
return next(
(room for channel, room in self._room_cache[guildid].items() if room.data.ownerid == userid),
None
)
# ----- Room Commands -----
@cmds.hybrid_group(
name=_p('cmd:room', "room"),
description=_p('cmd:room|desc', "Base command group for private room configuration.")
)
@appcmds.guild_only()
async def room_group(self, ctx: LionContext):
...
@room_group.command(
name=_p('cmd:room_rent', "rent"),
description=_p(
'cmd:room_rent|desc',
"Rent a private voice channel with LionCoins."
)
)
@appcmds.rename(
days=_p('cmd:room_rent|param:days', "days"),
members=_p('cmd:room_rent|param:members', "members"),
name=_p('cmd:room_rent|param:name', "name"),
)
@appcmds.describe(
days=_p(
'cmd:room_rent|param:days|desc',
"Number of days to pre-purchase. (Default: 1)"
),
members=_p(
'cmd:room_rent|param:members|desc',
"Mention the members you want to add to your private room."
),
name=_p(
'cmd:room_rent|param:name|desc',
"Name of your private voice channel."
)
)
async def room_rent_cmd(self, ctx: LionContext,
days: Optional[Range[int, 1, 30]] = 1,
members: Optional[str] = None,
name: Optional[Range[str, 1, 100]] = None,):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# Check renting is set up, with permissions
category: discord.CategoryChannel = ctx.lguild.config.get(RoomSettings.Category.setting_id).value
if category is None:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:not_setup',
"The private room system has not been set up! "
"A private room category needs to be set first with `/admin config rooms`."
))
), ephemeral=True
)
return
if not category.permissions_for(ctx.guild.me).manage_channels:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:insufficient_perms',
"I do not have enough permissions to create a new channel under "
"the configured private room category!"
))
), ephemeral=True
)
return
# Check that the author doesn't already own a room
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is not None and room.channel:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:room_exists',
"You already own a private room! Click to visit: {channel}"
)).format(channel=room.channel.mention)
), ephemeral=True
)
return
# Check that provided members actually exist
memberids = set(parse_members(members)) if members else set()
memberids.discard(ctx.author.id)
provided = []
for mid in memberids:
member = ctx.guild.get_member(mid)
if not member:
try:
member = await ctx.guild.fetch_member(mid)
except discord.HTTPException:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:member_not_found',
"Could not find the requested member {mention} in this server!"
)).format(mention=f"<@{mid}>")
), ephemeral=True
)
return
provided.append(member)
# Check provided members don't go over cap
cap = ctx.lguild.config.get(RoomSettings.MemberLimit.setting_id).value
if len(provided) >= cap:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:too_many_members',
"Too many members! You have requested to add `{count}` members to your room, "
"but the maximum private room size is `{cap}`!"
)).format(count=len(provided), cap=cap),
),
ephemeral=True
)
return
# Balance checks
rent = ctx.lguild.config.get(RoomSettings.Rent.setting_id).value
required = rent * days
# Purchase confirmation
confirm_msg = t(_np(
'cmd:room_rent|confirm:purchase',
"Are you sure you want to spend {coin}**{required}** to "
"rent a private room for `one` day?",
"Are you sure you want to spend {coin}**{required}** to "
"rent a private room for `{days}` days?",
days
)).format(
coin=self.bot.config.emojis.coin,
required=required,
days=days
)
confirm = Confirm(confirm_msg, ctx.author.id)
try:
result = await confirm.ask(ctx.interaction, ephemeral=True)
except ResponseTimedOut:
result = False
if not result:
return
# Positive response. Start a transaction.
room = await self._do_create_room(ctx, required, days, rent, name, provided)
if room:
# Ack with confirmation message pointing to the room
msg = t(_p(
'cmd:room_rent|success',
"Successfully created your private room {channel}!"
)).format(channel=room.channel.mention)
await ctx.reply(
embed=discord.Embed(
colour=discord.Colour.brand_green(),
title=t(_p('cmd:room_rent|success|title', "Private Room Created!")),
description=msg
)
)
self._start(room)
# Send tips message
tips = (
"Welcome to your very own private room {owner}!\n"
"You may use the control panel below to quickly configure your room, including:\n"
"- Inviting (and removing) members,\n"
"- Depositing LionCoins into the room bank to pay the daily rent, and\n"
"- Adding your very own Pomodoro timer to the room.\n\n"
"You also have elevated Discord permissions over the room itself!\n"
"This includes managing messages, and changing the name, region,"
" and bitrate of the channel, or even deleting the room entirely!"
" (Beware you will not be refunded in this case.)\n\n"
"Finally, you now have access to some new commands:\n"
"{status_cmd}: This brings up the room control panel again,"
" in case the interface below times out or is deleted/hidden.\n"
"{deposit_cmd}: Room members may use this command to easily contribute LionCoins to the room bank.\n"
"{invite_cmd} and {kick_cmd}: Quickly invite (or remove) multiple members by mentioning them.\n"
"{transfer_cmd}: Transfer the room to another owner, keeping the balance (this is not reversible!)"
).format(
owner=ctx.author.mention,
status_cmd=self.bot.core.mention_cmd('room status'),
deposit_cmd=self.bot.core.mention_cmd('room deposit'),
invite_cmd=self.bot.core.mention_cmd('room invite'),
kick_cmd=self.bot.core.mention_cmd('room kick'),
transfer_cmd=self.bot.core.mention_cmd('room transfer'),
)
await room.channel.send(tips)
# Send config UI
ui = RoomUI(self.bot, room, callerid=ctx.author.id, timeout=None)
await ui.send(room.channel)
@log_wrap(action='create_room')
async def _do_create_room(self, ctx, required, days, rent, name, provided) -> Optional[Room]:
t = self.bot.translator.t
# TODO: Rollback the channel create if this fails
async with self.bot.db.connection() as conn:
self.bot.db.conn = conn
# Note that the room creation will go into the UI as well.
async with conn.transaction():
# Check member balance is sufficient
await ctx.alion.data.refresh()
member_balance = ctx.alion.data.coins
if member_balance < required:
await ctx.reply(
embed=error_embed(
t(_np(
'cmd:room_rent|error:insufficient_funds',
"Renting a private room for `one` day costs {coin}**{required}**, "
"but you only have {coin}**{balance}**!",
"Renting a private room for `{days}` days costs {coin}**{required}**, "
"but you only have {coin}**{balance}**!",
days
)).format(
coin=self.bot.config.emojis.coin,
balance=member_balance,
required=required,
days=days
),
ephemeral=True
)
)
return
# Deduct balance
# TODO: Economy transaction instead of manual deduction
await ctx.alion.data.update(coins=CoreData.Member.coins - required)
# Create room with given starting balance and other parameters
try:
return await self.create_private_room(
ctx.guild,
ctx.author,
required - rent,
name or ctx.author.display_name,
members=provided
)
except discord.Forbidden:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:my_permissions',
"Could not create your private room! You were not charged.\n"
"I have insufficient permissions to create a private room channel."
)),
)
)
await ctx.alion.data.update(coins=CoreData.Member.coins + required)
except discord.HTTPException as e:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_rent|error:unknown',
"Could not create your private room! You were not charged.\n"
"An unknown error occurred while creating your private room.\n"
"`{error}`"
)).format(error=e.text),
)
)
await ctx.alion.data.update(coins=CoreData.Member.coins + required)
@room_group.command(
name=_p('cmd:room_status', "status"),
description=_p(
'cmd:room_status|desc',
"Display the status of your current room."
)
)
async def room_status_cmd(self, ctx: LionContext):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# Resolve target room
# Resolve order: Current channel, then owned room
room = self.get_channel_room(ctx.channel.id)
if room is None:
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is None:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_status|error:no_target',
"Could not identify target private room! Please re-run the command "
"in the private room you wish to view the status of."
))
),
ephemeral=True
)
return
# Respond with room UI
# Ephemeral UI unless we are in the room
ui = RoomUI(self.bot, room, callerid=ctx.author.id)
await ui.run(ctx.interaction, ephemeral=(ctx.channel.id != room.data.channelid))
await ui.wait()
@room_group.command(
name=_p('cmd:room_invite', "invite"),
description=_p(
'cmd:room_invite|desc',
"Add members to your private room."
)
)
@appcmds.rename(
members=_p('cmd:room_invite|param:members', "members"),
)
@appcmds.describe(
members=_p(
'cmd:room_invite|param:members|desc',
"Mention the members you want to add."
)
)
async def room_invite_cmd(self, ctx: LionContext, members: str):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# Resolve target room
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is None:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_invite|error:no_room',
"You do not own a private room! Use `/room rent` to rent one with {coin}!"
)).format(coin=self.bot.config.emojis.coin)),
ephemeral=True
)
return
# Check that provided members actually exist
memberids = set(parse_members(members)) if members else set()
memberids.discard(ctx.author.id)
memberids.difference_update(room.members)
provided = []
for mid in memberids:
member = ctx.guild.get_member(mid)
if not member:
try:
member = await ctx.guild.fetch_member(mid)
except discord.HTTPException:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_invite|error:member_not_found',
"Could not find the invited member {mention} in this server!"
)).format(mention=f"<@{mid}>")
), ephemeral=True
)
return
provided.append(member)
if not provided:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_invite|error:no_new_members',
"All members mentioned are already in the room!"
))
),
ephemeral=True
)
return
# Check provided members don't go over cap
cap = ctx.lguild.config.get(RoomSettings.MemberLimit.setting_id).value
if len(room.members) + len(provided) >= cap:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_invite|error:too_many_members',
"Too many members! You have invited `{count}` new members to your room, "
"but you already have `{current}`, "
"and the member cap is `{cap}`!"
)).format(
count=len(provided),
current=len(room.members) + 1,
cap=cap
),
),
ephemeral=True
)
return
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
# Finally, add the members
await room.add_new_members([m.id for m in provided])
# And ack
if ctx.channel.id != room.data.channelid:
embed = discord.Embed(
colour=discord.Colour.brand_green(),
title=t(_p(
'cmd:room_invite|success|ack',
"Members Invited successfully."
))
)
await ctx.reply(embed=embed)
else:
await ctx.interaction.delete_original_response()
@room_group.command(
name=_p('cmd:room_kick', "kick"),
description=_p(
'cmd:room_kick|desc',
"Remove a members from your private room."
)
)
@appcmds.rename(
members=_p('cmd:room_kick|param:members', "members")
)
@appcmds.describe(
members=_p(
'cmd:room_kick|param:members|desc',
"Mention the members you want to remove. Also accepts space-separated user ids."
)
)
async def room_kick_cmd(self, ctx: LionContext, members: str):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# Resolve target room
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is None:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_kick|error:no_room',
"You do not own a private room! Use `/room rent` to rent one with {coin}!"
)).format(coin=self.bot.config.emojis.coin)),
ephemeral=True
)
return
# Only remove members which are actually in the room
# Also ignore the owner
memberids = set(parse_members(members)) if members else set()
if ctx.guild.me.id in memberids:
await ctx.reply("Ouch, what did I do?")
memberids.intersection_update(room.members)
if not memberids:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_kick|error:no_matching_members',
"None of the mentioned members are in this room!"
))
),
ephemeral=True
)
return
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
# Finally, add the members
await room.rm_members(memberids)
# And ack
embed = discord.Embed(
colour=discord.Colour.brand_green(),
title=t(_p(
'cmd:room_kick|success|ack',
"Members removed."
))
)
await ctx.reply(embed=embed)
@room_group.command(
name=_p('cmd:room_transfer', "transfer"),
description=_p(
'cmd:room_transfer|desc',
"Transfer your private room to another room member. Not reversible!"
)
)
@appcmds.rename(
new_owner=_p('cmd:room_transfer|param:new_owner', "new_owner")
)
@appcmds.describe(
new_owner=_p(
'cmd:room_transfer|param:new_owner',
"The room member you would like to transfer your room to."
)
)
async def room_transfer_cmd(self, ctx: LionContext, new_owner: discord.Member):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# Resolve target room
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is None:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_transfer|error:no_room',
"You do not own a private room to transfer!"
)).format(coin=self.bot.config.emojis.coin)),
ephemeral=True
)
return
# Check if the target owner is actually a member of the room
if new_owner.id not in room.members:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_transfer|error:target_not_member',
"{mention} is not a member of your private room! You must invite them first."
)).format(mention=new_owner)
), ephemeral=True)
return
# Check if target owner already has a room
new_owner_room = self.get_owned_room(ctx.guild.id, new_owner.id)
if new_owner_room is not None:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:room_transfer|error:target_has_room',
"{mention} already owns a room! Members can only own one room at a time."
)).format(mention=new_owner.mention)
), ephemeral=True
)
return
# Confirm transfer
confirm_msg = t(_p(
'cmd:room_transfer|confirm|question',
"Are you sure you wish to transfer your private room {channel} to {new_owner}? "
"This action is not reversible!"
)).format(channel=room.channel, new_owner=new_owner.mention)
confirm = Confirm(confirm_msg, ctx.author.id)
try:
result = await confirm.ask(ctx.interaction, ephemeral=True)
except ResponseTimedOut:
result = False
if not result:
return
# Finally, do the transfer
await room.transfer_ownership(new_owner)
# Ack
await ctx.reply(
embed=discord.Embed(
colour=discord.Colour.brand_green(),
description=t(_p(
'cmd:room_transfer|success|description',
"You have successfully transferred ownership of {channel} to {new_owner}."
)).format(channel=room.channel, new_owner=new_owner.mention)
)
)
@room_group.command(
name=_p('cmd:room_deposit', "deposit"),
description=_p(
'cmd:room_deposit|desc',
"Deposit LionCoins in your private room bank to add more days. (Members may also deposit!)"
)
)
@appcmds.rename(
coins=_p('cmd:room_deposit|param:coins', "coins")
)
@appcmds.describe(
coins=_p(
'cmd:room_deposit|param:coins|desc',
"Number of coins to deposit."
)
)
async def room_deposit_cmd(self, ctx: LionContext, coins: Range[int, 1, MAX_COINS]):
t = self.bot.translator.t
if not ctx.guild or not ctx.interaction:
return
# All responses will be ephemeral
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
# Resolve target room
# Resolve order: Current channel, then owned room
room = self.get_channel_room(ctx.channel.id)
if room is None:
room = self.get_owned_room(ctx.guild.id, ctx.author.id)
if room is None:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_deposit|error:no_target',
"Could not identify target private room! Please re-run the command "
"in the private room you wish to contribute to."
))
),
ephemeral=True
)
return
# Start Transaction
# TODO: Economy transaction
await ctx.alion.data.refresh()
member_balance = ctx.alion.data.coins
if member_balance < coins:
await ctx.reply(
embed=error_embed(t(_p(
'cmd:room_deposit|error:insufficient_funds',
"You cannot deposit {coin}**{amount}**! You only have {coin}**{balance}**."
)).format(
coin=self.bot.config.emojis.coin,
amount=coins,
balance=member_balance
)),
ephemeral=True
)
return
# Deduct balance
await ctx.alion.data.update(coins=CoreData.Member.coins - coins)
await room.data.update(coin_balance=RoomData.Room.coin_balance + coins)
# Post deposit message
await room.notify_deposit(ctx.author, coins)
# Ack the deposit
if ctx.channel.id != room.data.channelid:
ack_msg = t(_p(
'cmd:room_depost|success',
"Success! You have contributed {coin}**{amount}** to the private room bank."
)).format(coin=self.bot.config.emojis.coin, amount=coins)
await ctx.reply(
embed=discord.Embed(colour=discord.Colour.brand_green(), description=ack_msg)
)
else:
await ctx.interaction.delete_original_response()
# ----- Guild Configuration -----
@LionCog.placeholder_group
@cmds.hybrid_group('configure', with_app_commands=False)
async def configure_group(self, ctx: LionContext):
...
@configure_group.command(
name=_p('cmd:configure_rooms', "rooms"),
description=_p('cmd:configure_rooms|desc', "Configure Rented Private Rooms")
)
@appcmds.rename(
**{setting.setting_id: setting._display_name for setting in RoomSettings.model_settings}
)
@appcmds.describe(
**{setting.setting_id: setting._desc for setting in RoomSettings.model_settings}
)
@high_management_ward
async def configure_rooms_cmd(self, ctx: LionContext,
rooms_category: Optional[discord.CategoryChannel] = None,
rooms_price: Optional[Range[int, 0, MAX_COINS]] = None,
rooms_slots: Optional[Range[int, 1, MAX_COINS]] = None,
rooms_visible: Optional[bool] = None):
# t = self.bot.translator.t
# Type checking guards
if not ctx.guild:
return
if not ctx.interaction:
return
# TODO: Value verification on the category channel for permissions
await ctx.interaction.response.defer(thinking=True)
provided = {
'rooms_category': rooms_category,
'rooms_price': rooms_price,
'rooms_slots': rooms_slots,
'rooms_visible': rooms_visible
}
modified = {(sid, val) for sid, val in provided.items() if val is not None}
if modified:
lines = []
update_args = {}
settings = []
for setting_id, value in modified:
setting = ctx.lguild.config.get(setting_id)
setting.value = value
settings.append(setting)
update_args[setting._column] = setting._data
lines.append(setting.update_message)
# Data update
await ctx.lguild.data.update(**update_args)
for setting in settings:
setting.dispatch_update()
# Ack modified
tick = self.bot.config.emojis.tick
embed = discord.Embed(
colour=discord.Colour.brand_green(),
description='\n'.join(f"{tick} {line}" for line in lines)
)
await ctx.reply(embed=embed)
if ctx.channel.id not in RoomSettingUI._listening or not modified:
ui = RoomSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
await ui.run(ctx.interaction)
await ui.wait()