rewrite: New private Room system.
This commit is contained in:
@@ -47,6 +47,7 @@ loading = <:cyclebigger:975880828611600404>
|
|||||||
tick = :✅:
|
tick = :✅:
|
||||||
clock = :⏱️:
|
clock = :⏱️:
|
||||||
warning = :⚠️:
|
warning = :⚠️:
|
||||||
|
config = :⚙️:
|
||||||
|
|
||||||
coin = <:coin:975880967485022239>
|
coin = <:coin:975880967485022239>
|
||||||
|
|
||||||
|
|||||||
@@ -731,7 +731,6 @@ CREATE TABLE season_stats(
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- Pomodoro Data {{{
|
-- Pomodoro Data {{{
|
||||||
|
|
||||||
ALTER TABLE timers ADD COLUMN ownerid BIGINT REFERENCES user_config;
|
ALTER TABLE timers ADD COLUMN ownerid BIGINT REFERENCES user_config;
|
||||||
ALTER TABLE timers ADD COLUMN manager_roleid BIGINT;
|
ALTER TABLE timers ADD COLUMN manager_roleid BIGINT;
|
||||||
ALTER TABLE timers ADD COLUMN last_messageid BIGINT;
|
ALTER TABLE timers ADD COLUMN last_messageid BIGINT;
|
||||||
@@ -739,6 +738,64 @@ ALTER TABLE timers ADD COLUMN voice_alerts BOOLEAN;
|
|||||||
ALTER TABLE timers ADD COLUMN auto_restart BOOLEAN;
|
ALTER TABLE timers ADD COLUMN auto_restart BOOLEAN;
|
||||||
ALTER TABLE timers RENAME COLUMN text_channelid TO notification_channelid;
|
ALTER TABLE timers RENAME COLUMN text_channelid TO notification_channelid;
|
||||||
ALTER TABLE timers ALTER COLUMN last_started DROP NOT NULL;
|
ALTER TABLE timers ALTER COLUMN last_started DROP NOT NULL;
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
-- Rented Room Data {{{
|
||||||
|
/* OLD SCHEMA
|
||||||
|
CREATE TABLE rented(
|
||||||
|
channelid BIGINT PRIMARY KEY,
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
ownerid BIGINT NOT NULL,
|
||||||
|
expires_at TIMESTAMP DEFAULT ((now() at time zone 'utc') + INTERVAL '1 day'),
|
||||||
|
created_at TIMESTAMP DEFAULT (now() at time zone 'utc'),
|
||||||
|
FOREIGN KEY (guildid, ownerid) REFERENCES members (guildid, userid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX rented_owners ON rented (guildid, ownerid);
|
||||||
|
|
||||||
|
CREATE TABLE rented_members(
|
||||||
|
channelid BIGINT NOT NULL REFERENCES rented(channelid) ON DELETE CASCADE,
|
||||||
|
userid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX rented_members_channels ON rented_members (channelid);
|
||||||
|
CREATE INDEX rented_members_users ON rented_members (userid);
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* NEW SCHEMA
|
||||||
|
CREATE TABLE rented_rooms(
|
||||||
|
channelid BIGINT PRIMARY KEY,
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
ownerid BIGINT NOT NULL,
|
||||||
|
coin_balance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
name TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
last_tick TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
FOREIGN KEY (guildid, ownerid) REFERENCES members (guildid, userid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX rented_owners ON rented (guildid, ownerid);
|
||||||
|
|
||||||
|
CREATE TABLE rented_members(
|
||||||
|
channelid BIGINT NOT NULL REFERENCES rented(channelid) ON DELETE CASCADE,
|
||||||
|
userid BIGINT NOT NULL,
|
||||||
|
contribution INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX rented_members_channels ON rented_members (channelid);
|
||||||
|
CREATE INDEX rented_members_users ON rented_members (userid);
|
||||||
|
*/
|
||||||
|
|
||||||
|
ALTER TABLE rented RENAME TO rented_rooms;
|
||||||
|
ALTER TABLE rented_rooms DROP COLUMN expires_at;
|
||||||
|
ALTER TABLE rented_rooms ALTER COLUMN created_at TYPE TIMESTAMPTZ;
|
||||||
|
ALTER TABLE rented_rooms ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE rented_rooms ADD COLUMN coin_balance INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE rented_rooms ADD COLUMN name TEXT;
|
||||||
|
ALTER TABLE rented_rooms ADD COLUMN last_tick TIMESTAMPTZ;
|
||||||
|
ALTER TABLE rented_members ADD COLUMN contribution INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
DROP INDEX rented_owners;
|
||||||
|
CREATE INDEX rented_owners ON rented_rooms(guildid, ownerid);
|
||||||
|
|
||||||
|
ALTER TABLE guild_config ADD COLUMN renting_visible BOOLEAN;
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ class CoreData(Registry, name="core"):
|
|||||||
renting_cap = Integer()
|
renting_cap = Integer()
|
||||||
renting_role = Integer()
|
renting_role = Integer()
|
||||||
renting_sync_perms = Bool()
|
renting_sync_perms = Bool()
|
||||||
|
renting_visible = Bool()
|
||||||
|
|
||||||
accountability_category = Integer()
|
accountability_category = Integer()
|
||||||
accountability_lobby = Integer()
|
accountability_lobby = Integer()
|
||||||
|
|||||||
@@ -256,11 +256,11 @@ class RowModel:
|
|||||||
return cls.table.fetch_rows_where(*args, **kwargs)
|
return cls.table.fetch_rows_where(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch(cls: Type[RowT], *rowid) -> Optional[RowT]:
|
async def fetch(cls: Type[RowT], *rowid, cached=True) -> Optional[RowT]:
|
||||||
"""
|
"""
|
||||||
Fetch the row with the given id, retrieving from cache where possible.
|
Fetch the row with the given id, retrieving from cache where possible.
|
||||||
"""
|
"""
|
||||||
row = cls._cache_.get(rowid, None)
|
row = cls._cache_.get(rowid, None) if cached else None
|
||||||
if row is None:
|
if row is None:
|
||||||
rows = await cls.fetch_where(**cls._dict_from_id(rowid))
|
rows = await cls.fetch_where(**cls._dict_from_id(rowid))
|
||||||
row = rows[0] if rows else None
|
row = rows[0] if rows else None
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ active = [
|
|||||||
'.tasklist',
|
'.tasklist',
|
||||||
'.statistics',
|
'.statistics',
|
||||||
'.pomodoro',
|
'.pomodoro',
|
||||||
|
'.rooms',
|
||||||
'.test',
|
'.test',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from data import ORDER
|
|||||||
from utils.ui import Confirm, Pager
|
from utils.ui import Confirm, Pager
|
||||||
from utils.lib import error_embed, MessageArgs, utc_now
|
from utils.lib import error_embed, MessageArgs, utc_now
|
||||||
from wards import low_management
|
from wards import low_management
|
||||||
|
from constants import MAX_COINS
|
||||||
|
|
||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import EconomyData, TransactionType, AdminActionType
|
from .data import EconomyData, TransactionType, AdminActionType
|
||||||
@@ -21,9 +22,6 @@ from .settingui import EconomyConfigUI
|
|||||||
_, _p, _np = babel._, babel._p, babel._np
|
_, _p, _np = babel._, babel._p, babel._np
|
||||||
|
|
||||||
|
|
||||||
MAX_COINS = 2**16
|
|
||||||
|
|
||||||
|
|
||||||
class Economy(LionCog):
|
class Economy(LionCog):
|
||||||
"""
|
"""
|
||||||
Commands
|
Commands
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ class TimerCog(LionCog):
|
|||||||
|
|
||||||
@LionCog.listener('on_guildset_pomodoro_channel')
|
@LionCog.listener('on_guildset_pomodoro_channel')
|
||||||
@log_wrap(action='Update Pomodoro Channels')
|
@log_wrap(action='Update Pomodoro Channels')
|
||||||
async def _update_pomodoro_channels(self, guildid: int, setting: TimerSettings.PomodoroChannel):
|
async def _update_pomodoro_channels(self, guildid: int, data: Optional[int]):
|
||||||
"""
|
"""
|
||||||
Request a send_status for all guild timers which need to move channel.
|
Request a send_status for all guild timers which need to move channel.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
from .status import TimerStatusUI
|
from .status import TimerStatusUI
|
||||||
|
from .edit import TimerEditor
|
||||||
|
from .config import TimerOptionsUI
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class TimerOptionsUI(MessageUI):
|
|||||||
self.refresh_delete_button(),
|
self.refresh_delete_button(),
|
||||||
)
|
)
|
||||||
self.set_layout(
|
self.set_layout(
|
||||||
(self.edit_button, self.voice_button, self.close_button)
|
(self.edit_button, self.voice_button, self.delete_button, self.close_button)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
|
|||||||
10
src/modules/rooms/__init__.py
Normal file
10
src/modules/rooms/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
from babel.translator import LocalBabel
|
||||||
|
|
||||||
|
logger = logging.getLogger('Rooms')
|
||||||
|
babel = LocalBabel('rooms')
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import RoomCog
|
||||||
|
await bot.add_cog(RoomCog(bot))
|
||||||
941
src/modules/rooms/cog.py
Normal file
941
src/modules/rooms/cog.py
Normal file
@@ -0,0 +1,941 @@
|
|||||||
|
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 low_management
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
_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.configure_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()
|
||||||
|
|
||||||
|
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:
|
||||||
|
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, data: Optional[int]):
|
||||||
|
"""
|
||||||
|
Move all active private channels to the new category.
|
||||||
|
|
||||||
|
This shouldn't affect the channel function at all.
|
||||||
|
"""
|
||||||
|
guild = self.bot.get_guild(guildid)
|
||||||
|
new_category = guild.get_channel(data) if guild 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, data: bool):
|
||||||
|
"""
|
||||||
|
Update the everyone override on each room to reflect the new setting.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build permission overwrites for owner and members, take into account visible setting
|
||||||
|
overwrites = {
|
||||||
|
owner: owner_overwrite,
|
||||||
|
guild.default_role: everyone_overwrite
|
||||||
|
}
|
||||||
|
for member in members:
|
||||||
|
overwrites[member] = member_overwrite
|
||||||
|
|
||||||
|
# Create channel
|
||||||
|
channel = await guild.create_voice_channel(
|
||||||
|
name=name,
|
||||||
|
reason=f"Creating Private Room for {owner.id}",
|
||||||
|
category=lguild.config.get(RoomSettings.Category.setting_id).value,
|
||||||
|
overwrites=overwrites
|
||||||
|
)
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
self._start(room)
|
||||||
|
|
||||||
|
# Send tips message
|
||||||
|
await channel.send("{mention} welcome to your private room! (TBD TIPS HERE)".format(mention=owner.mention))
|
||||||
|
|
||||||
|
# Send config UI
|
||||||
|
ui = RoomUI(self.bot, room, callerid=owner.id, timeout=None)
|
||||||
|
await ui.send(channel)
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 `/configure 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(member=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.
|
||||||
|
conn = await self.bot.db.get_connection()
|
||||||
|
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
|
||||||
|
room = await self.create_private_room(
|
||||||
|
ctx.guild,
|
||||||
|
ctx.author,
|
||||||
|
required - rent,
|
||||||
|
name or ctx.author.display_name,
|
||||||
|
members=provided
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@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(member=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
|
||||||
|
conn = await self.bot.db.get_connection()
|
||||||
|
async with conn.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
|
||||||
|
# TODO: Economy transaction
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
@cmds.check(low_management)
|
||||||
|
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()
|
||||||
48
src/modules/rooms/data.py
Normal file
48
src/modules/rooms/data.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from data import Registry, RowModel
|
||||||
|
from data.columns import Integer, Timestamp, String
|
||||||
|
|
||||||
|
|
||||||
|
class RoomData(Registry):
|
||||||
|
class Room(RowModel):
|
||||||
|
"""
|
||||||
|
CREATE TABLE rented_rooms(
|
||||||
|
channelid BIGINT PRIMARY KEY,
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
ownerid BIGINT NOT NULL,
|
||||||
|
coin_balance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
name TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
last_tick TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
FOREIGN KEY (guildid, ownerid) REFERENCES members (guildid, userid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX rented_owners ON rented (guildid, ownerid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'rented_rooms'
|
||||||
|
|
||||||
|
channelid = Integer(primary=True)
|
||||||
|
guildid = Integer()
|
||||||
|
ownerid = Integer()
|
||||||
|
coin_balance = Integer()
|
||||||
|
name = String()
|
||||||
|
created_at = Timestamp()
|
||||||
|
last_tick = Timestamp()
|
||||||
|
deleted_at = Timestamp()
|
||||||
|
|
||||||
|
class RoomMember(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE rented_members(
|
||||||
|
channelid BIGINT NOT NULL REFERENCES rented(channelid) ON DELETE CASCADE,
|
||||||
|
userid BIGINT NOT NULL,
|
||||||
|
contribution INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX rented_members_channels ON rented_members (channelid);
|
||||||
|
CREATE INDEX rented_members_users ON rented_members (userid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'rented_members'
|
||||||
|
|
||||||
|
channelid = Integer(primary=True)
|
||||||
|
userid = Integer(primary=True)
|
||||||
|
contribution = Integer()
|
||||||
41
src/modules/rooms/lib.py
Normal file
41
src/modules/rooms/lib.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import discord
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def parse_members(memberstr: str) -> list[int]:
|
||||||
|
"""
|
||||||
|
Parse a mixed list of ids and mentions into a list of memberids.
|
||||||
|
"""
|
||||||
|
if memberstr:
|
||||||
|
memberids = [int(x) for x in re.findall(r'[<@!\s]*([0-9]{15,20})[>\s,]*', memberstr)]
|
||||||
|
else:
|
||||||
|
memberids = []
|
||||||
|
return memberids
|
||||||
|
|
||||||
|
|
||||||
|
owner_overwrite = discord.PermissionOverwrite(
|
||||||
|
view_channel=True,
|
||||||
|
manage_channels=True,
|
||||||
|
manage_webhooks=True,
|
||||||
|
attach_files=True,
|
||||||
|
embed_links=True,
|
||||||
|
add_reactions=True,
|
||||||
|
manage_messages=True,
|
||||||
|
create_public_threads=True,
|
||||||
|
create_private_threads=True,
|
||||||
|
manage_threads=True,
|
||||||
|
connect=True,
|
||||||
|
speak=True,
|
||||||
|
stream=True,
|
||||||
|
use_application_commands=True,
|
||||||
|
use_embedded_activities=True,
|
||||||
|
move_members=True,
|
||||||
|
external_emojis=True
|
||||||
|
)
|
||||||
|
member_overwrite = discord.PermissionOverwrite(
|
||||||
|
view_channel=True,
|
||||||
|
send_messages=True,
|
||||||
|
connect=True,
|
||||||
|
speak=True,
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
278
src/modules/rooms/room.py
Normal file
278
src/modules/rooms/room.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
from meta.logger import log_wrap, log_context
|
||||||
|
from utils.lib import utc_now
|
||||||
|
from core.lion_guild import LionGuild
|
||||||
|
from babel.translator import ctx_locale
|
||||||
|
|
||||||
|
from modules.pomodoro.cog import TimerCog
|
||||||
|
from modules.pomodoro.timer import Timer
|
||||||
|
|
||||||
|
from . import babel, logger
|
||||||
|
from .data import RoomData
|
||||||
|
from .roomui import RoomUI
|
||||||
|
from .lib import owner_overwrite, member_overwrite
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class Room:
|
||||||
|
__slots__ = ('bot', 'data', 'lguild', 'members', '_tick_wait')
|
||||||
|
|
||||||
|
tick_length = timedelta(days=1)
|
||||||
|
# tick_length = timedelta(hours=1)
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot, data: RoomData.Room, lguild: LionGuild, members: list[int]):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = data
|
||||||
|
self.lguild = lguild
|
||||||
|
self.members = members
|
||||||
|
|
||||||
|
log_context.set(f"cid: {self.data.channelid}")
|
||||||
|
|
||||||
|
# State
|
||||||
|
self._tick_wait: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self) -> Optional[discord.VoiceChannel]:
|
||||||
|
"""
|
||||||
|
Discord Channel which this room lives in.
|
||||||
|
"""
|
||||||
|
return self.bot.get_channel(self.data.channelid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timer(self) -> Optional[Timer]:
|
||||||
|
timer_cog: TimerCog = self.bot.get_cog('TimerCog')
|
||||||
|
if timer_cog is not None:
|
||||||
|
return timer_cog.get_channel_timer(self.data.channelid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_tick(self):
|
||||||
|
return self.data.last_tick or self.data.created_at
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_tick(self):
|
||||||
|
return self.last_tick + self.tick_length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rent(self):
|
||||||
|
return self.lguild.config.get('rooms_price').value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expiring(self):
|
||||||
|
return self.rent > self.data.coin_balance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deleted(self):
|
||||||
|
return bool(self.data.deleted_at)
|
||||||
|
|
||||||
|
async def notify_deposit(self, member: discord.Member, amount: int):
|
||||||
|
# Assumes locale is set correctly
|
||||||
|
t = self.bot.translator.t
|
||||||
|
notification = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=t(_p(
|
||||||
|
'room|notify:deposit|description',
|
||||||
|
"{member} has deposited {coin}**{amount}** into the room bank!"
|
||||||
|
)).format(member=member.mention, coin=self.bot.config.emojis.coin, amount=amount)
|
||||||
|
)
|
||||||
|
if self.channel:
|
||||||
|
try:
|
||||||
|
await self.channel.send(embed=notification)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def add_new_members(self, memberids):
|
||||||
|
member_data = self.bot.get_cog('RoomCog').data.RoomMember
|
||||||
|
await member_data.table.insert_many(
|
||||||
|
('channelid', 'userid'),
|
||||||
|
*((self.data.channelid, memberid) for memberid in memberids)
|
||||||
|
)
|
||||||
|
self.members.extend(memberids)
|
||||||
|
t = self.bot.translator.t
|
||||||
|
notification = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
title=t(_p(
|
||||||
|
'room|notify:new_members|title',
|
||||||
|
"New Members!"
|
||||||
|
)),
|
||||||
|
description=t(_p(
|
||||||
|
'room|notify:new_members|desc',
|
||||||
|
"Welcome {members}"
|
||||||
|
)).format(members=', '.join(f"<@{mid}>" for mid in memberids))
|
||||||
|
)
|
||||||
|
if self.channel:
|
||||||
|
try:
|
||||||
|
await self.channel.send(embed=notification)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def rm_members(self, memberids):
|
||||||
|
member_data = self.bot.get_cog('RoomCog').data.RoomMember
|
||||||
|
await member_data.table.delete_where(channelid=self.data.channelid, userid=list(memberids))
|
||||||
|
self.members = list(set(self.members).difference(memberids))
|
||||||
|
# No need to notify for removal
|
||||||
|
return
|
||||||
|
|
||||||
|
async def transfer_ownership(self, new_owner):
|
||||||
|
member_data = self.bot.get_cog('RoomCog').data.RoomMember
|
||||||
|
old_ownerid = self.data.ownerid
|
||||||
|
|
||||||
|
# Add old owner as a member
|
||||||
|
await member_data.create(channelid=self.data.channelid, userid=old_ownerid)
|
||||||
|
self.members.append(old_ownerid)
|
||||||
|
|
||||||
|
# Remove new owner from the members
|
||||||
|
await member_data.table.delete_where(channelid=self.data.channelid, userid=new_owner.id)
|
||||||
|
self.members.remove(new_owner.id)
|
||||||
|
|
||||||
|
# Change room owner
|
||||||
|
await self.data.update(ownerid=new_owner.id)
|
||||||
|
|
||||||
|
if self.channel:
|
||||||
|
try:
|
||||||
|
# Update overwrite for old owner
|
||||||
|
if old_owner := self.channel.guild.get_member(old_ownerid):
|
||||||
|
await self.channel.set_permissions(
|
||||||
|
old_owner,
|
||||||
|
overwrite=member_overwrite
|
||||||
|
)
|
||||||
|
# Update overwrite for new owner
|
||||||
|
await self.channel.set_permissions(
|
||||||
|
new_owner,
|
||||||
|
overwrite=owner_overwrite
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
logger.warning(
|
||||||
|
"Exception while changing room ownership. Room overwrites may be incorrect.",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
# Notification
|
||||||
|
t = self.bot.translator.t
|
||||||
|
notification = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=t(_p(
|
||||||
|
'room|notify:transfer|description',
|
||||||
|
"{old_owner} has transferred private room ownership to {new_owner}"
|
||||||
|
)).format(old_owner=f"<@{old_ownerid}>", new_owner=new_owner.mention)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.channel.send(embed=notification)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@log_wrap(action="Room Runloop")
|
||||||
|
async def run(self):
|
||||||
|
"""
|
||||||
|
Tick loop.
|
||||||
|
|
||||||
|
Keeps scheduling ticks until expired or cancelled.
|
||||||
|
May be safely cancelled.
|
||||||
|
"""
|
||||||
|
if self._tick_wait and not self._tick_wait.done():
|
||||||
|
self._tick_wait.cancel()
|
||||||
|
|
||||||
|
while not self.deleted:
|
||||||
|
now = utc_now()
|
||||||
|
diff = (self.next_tick - now).total_seconds()
|
||||||
|
self._tick_wait = asyncio.create_task(asyncio.sleep(diff))
|
||||||
|
try:
|
||||||
|
await self._tick_wait
|
||||||
|
await asyncio.shield(self._tick())
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
f"Unhandled exception while ticking for room: {self.data!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@log_wrap(action="Room Tick")
|
||||||
|
async def _tick(self):
|
||||||
|
"""
|
||||||
|
Execute the once-per day room tick.
|
||||||
|
|
||||||
|
This deducts the rent amount from the room balance,
|
||||||
|
if the balance is insufficient, expires the room.
|
||||||
|
Posts a status message in the room channel when it does so.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
ctx_locale.set(self.lguild.config.get('guild_locale').value)
|
||||||
|
if self.deleted:
|
||||||
|
# Already deleted, nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Run tick
|
||||||
|
logger.debug(f"Tick running for room: {self.data!r}")
|
||||||
|
|
||||||
|
# Deduct balance
|
||||||
|
await self.data.update(
|
||||||
|
coin_balance=RoomData.Room.coin_balance - self.rent,
|
||||||
|
last_tick=utc_now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# If balance is negative, expire room, otherwise notify channel
|
||||||
|
if self.data.coin_balance < 0:
|
||||||
|
if owner := self.bot.get_user(self.data.ownerid):
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.red(),
|
||||||
|
title=t(_p(
|
||||||
|
'room|embed:expiry|title',
|
||||||
|
"Private Room Expired!"
|
||||||
|
)),
|
||||||
|
description=t(_p(
|
||||||
|
'room|embed:expiry|description',
|
||||||
|
"Your private room in **{guild}** has expired!"
|
||||||
|
)).format(guild=self.bot.get_guild(self.data.guildid))
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await owner.send(embed=embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await self.destroy(reason='Room Expired')
|
||||||
|
elif self.channel:
|
||||||
|
# Notify channel
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
description=self.bot.translator.t(_p(
|
||||||
|
'room|tick|rent_deducted',
|
||||||
|
"Daily rent deducted from room balance. New balance: {coin}**{amount}**"
|
||||||
|
)).format(
|
||||||
|
coin=self.bot.config.emojis.coin, amount=self.data.coin_balance
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.channel.send(embed=embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# No channel means room was deleted
|
||||||
|
# Just cleanup quietly
|
||||||
|
await self.destroy(reason='Channel Missing')
|
||||||
|
|
||||||
|
@log_wrap(action="Destroy Room")
|
||||||
|
async def destroy(self, reason: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Destroy the room.
|
||||||
|
|
||||||
|
Attempts to delete the voice channel and log destruction.
|
||||||
|
This is idempotent, so multiple events may trigger destroy.
|
||||||
|
"""
|
||||||
|
if self._tick_wait:
|
||||||
|
self._tick_wait.cancel()
|
||||||
|
|
||||||
|
if self.channel:
|
||||||
|
try:
|
||||||
|
await self.channel.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not self.deleted:
|
||||||
|
logger.info(
|
||||||
|
f"Destroying private room <cid: {self.data.channelid}> for reason '{reason}': {self.data!r}"
|
||||||
|
)
|
||||||
|
await self.data.update(deleted_at=utc_now())
|
||||||
461
src/modules/rooms/roomui.py
Normal file
461
src/modules/rooms/roomui.py
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
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 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():
|
||||||
|
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
|
||||||
|
amount = int(response)
|
||||||
|
await submit.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
# Start transaction for deposit
|
||||||
|
conn = await self.bot.db.get_connection()
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Post deposit message
|
||||||
|
await self.room.notify_deposit(press.user, amount)
|
||||||
|
|
||||||
|
await self.refresh(thinking=submit)
|
||||||
|
|
||||||
|
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 `/pomodoro edit` to configure further."
|
||||||
|
))
|
||||||
|
)
|
||||||
|
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` to increase balance."
|
||||||
|
)).format(
|
||||||
|
coin=conf.emojis.coin,
|
||||||
|
amount=balance,
|
||||||
|
rent=rent,
|
||||||
|
expiry=next_tick
|
||||||
|
)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
...
|
||||||
156
src/modules/rooms/settings.py
Normal file
156
src/modules/rooms/settings.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
from settings import ModelData
|
||||||
|
from settings.groups import SettingGroup
|
||||||
|
from settings.setting_types import ChannelSetting, IntegerSetting, BoolSetting
|
||||||
|
|
||||||
|
from meta import conf
|
||||||
|
from core.data import CoreData
|
||||||
|
from babel.translator import ctx_translator
|
||||||
|
|
||||||
|
from . import babel
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class RoomSettings(SettingGroup):
|
||||||
|
class Category(ModelData, ChannelSetting):
|
||||||
|
setting_id = 'rooms_category'
|
||||||
|
_event = 'guildset_rooms_category'
|
||||||
|
|
||||||
|
_display_name = _p(
|
||||||
|
'guildset:room_category', "rooms_category"
|
||||||
|
)
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:rooms_category|desc',
|
||||||
|
"Category in which to create private voice channels."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:room_category|long_desc',
|
||||||
|
"When a member uses `/room rent` to rent a new private room, "
|
||||||
|
"a private voice channel will be created under this category, "
|
||||||
|
"manageable by the member. "
|
||||||
|
"I must have permission to create new channels in this category, "
|
||||||
|
"as well as to manage permissions."
|
||||||
|
)
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.renting_category.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self) -> str:
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
value = self.value
|
||||||
|
if value is None:
|
||||||
|
# Shut down renting system
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_category|set_response:unset',
|
||||||
|
"The private room category has been unset. Existing private rooms will not be affected. "
|
||||||
|
"Delete the channels manually to remove the private rooms."
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_category|set_response:set',
|
||||||
|
"Private room category has been set to {channel}. Existing private rooms will be moved."
|
||||||
|
)).format(channel=self.value.mention)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
class Rent(ModelData, IntegerSetting):
|
||||||
|
setting_id = 'rooms_price'
|
||||||
|
_event = 'guildset_rooms_price'
|
||||||
|
|
||||||
|
_display_name = _p(
|
||||||
|
'guildset:rooms_price', "room_rent"
|
||||||
|
)
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:rooms_rent|desc',
|
||||||
|
"Daily rent price for a private room."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:rooms_rent|long_desc',
|
||||||
|
"Members will be charged this many LionCoins for each day they rent a private room."
|
||||||
|
)
|
||||||
|
_default = 1000
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.renting_price.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self) -> str:
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_price|set_response',
|
||||||
|
"Private rooms will now cost {coin}**{amount}}** per 24 hours."
|
||||||
|
)).format(
|
||||||
|
coin=conf.emojis.coin,
|
||||||
|
amount=self.value
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
class MemberLimit(ModelData, IntegerSetting):
|
||||||
|
setting_id = 'rooms_slots'
|
||||||
|
_event = 'guildset_rooms_slots'
|
||||||
|
|
||||||
|
_display_name = _p('guildset:rooms_slots', "room_member_cap")
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:rooms_slots|desc',
|
||||||
|
"Maximum number of members in each private room."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:rooms_slots|long_desc',
|
||||||
|
"Private room owners may invite other members to their private room via the UI, "
|
||||||
|
"or through the `/room invite` command. "
|
||||||
|
"This setting limits the maximum number of members a private room may hold."
|
||||||
|
)
|
||||||
|
_default = 25
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.renting_cap.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self) -> str:
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_slots|set_response',
|
||||||
|
"Private rooms are now capped to **{amount}** members."
|
||||||
|
)).format(amount=self.value)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
class Visible(ModelData, BoolSetting):
|
||||||
|
setting_id = 'rooms_visible'
|
||||||
|
_event = 'guildset_rooms_visible'
|
||||||
|
|
||||||
|
_display_name = _p('guildset:rooms_visible', "room_visibility")
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:rooms_visible|desc',
|
||||||
|
"Whether private rented rooms are visible to non-members."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:rooms_visible|long_desc',
|
||||||
|
"If enabled, new private rooms will be created with the `VIEW_CHANNEL` permission "
|
||||||
|
"enabled for the `@everyone` role."
|
||||||
|
)
|
||||||
|
_default = False
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.renting_visible.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self) -> str:
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
if self.value:
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_visible|set_response:enabled',
|
||||||
|
"Private rooms will now be visible to everyone."
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:rooms_visible|set_response:disabled',
|
||||||
|
"Private rooms will now only be visible to their members (and admins)."
|
||||||
|
))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
model_settings = (
|
||||||
|
Category,
|
||||||
|
Rent,
|
||||||
|
MemberLimit,
|
||||||
|
Visible,
|
||||||
|
)
|
||||||
101
src/modules/rooms/settingui.py
Normal file
101
src/modules/rooms/settingui.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
from discord.ui.select import select, ChannelSelect
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
|
||||||
|
from utils.ui import ConfigUI, DashboardSection
|
||||||
|
from utils.lib import MessageArgs
|
||||||
|
|
||||||
|
from .settings import RoomSettings
|
||||||
|
from . import babel
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class RoomSettingUI(ConfigUI):
|
||||||
|
setting_classes = RoomSettings.model_settings
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs):
|
||||||
|
self.settings = bot.get_cog('RoomCog').settings
|
||||||
|
super().__init__(bot, guildid, channelid, **kwargs)
|
||||||
|
|
||||||
|
# ----- UI Components -----
|
||||||
|
@select(cls=ChannelSelect, channel_types=[discord.ChannelType.category],
|
||||||
|
min_values=0, max_values=1,
|
||||||
|
placeholder='CATEGORY_PLACEHOLDER')
|
||||||
|
async def category_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||||
|
await selection.response.defer()
|
||||||
|
setting = self.instances[0]
|
||||||
|
setting.value = selected.values[0] if selected.values else None
|
||||||
|
await setting.write()
|
||||||
|
|
||||||
|
async def category_menu_refresh(self):
|
||||||
|
self.category_menu.placeholder = self.bot.translator.t(_p(
|
||||||
|
'ui:room_config|menu:category|placeholder',
|
||||||
|
"Select Private Room Category"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label="VISIBLE_BUTTON_PLACEHOLDER", style=ButtonStyle.grey)
|
||||||
|
async def visible_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
setting = next(inst for inst in self.instances if inst.setting_id == RoomSettings.Visible.setting_id)
|
||||||
|
setting.value = not setting.value
|
||||||
|
await setting.write()
|
||||||
|
|
||||||
|
async def visible_button_refresh(self):
|
||||||
|
button = self.visible_button
|
||||||
|
button.label = self.bot.translator.t(_p(
|
||||||
|
'ui:room_config|button:visible|label',
|
||||||
|
"Toggle Room Visibility"
|
||||||
|
))
|
||||||
|
setting = next(inst for inst in self.instances if inst.setting_id == RoomSettings.Visible.setting_id)
|
||||||
|
button.style = ButtonStyle.green if setting.value else ButtonStyle.grey
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
title = t(_p(
|
||||||
|
'ui:rooms_config|embed|title',
|
||||||
|
"Private Room System Configuration Panel"
|
||||||
|
))
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
title=title
|
||||||
|
)
|
||||||
|
for setting in self.instances:
|
||||||
|
embed.add_field(**setting.embed_field, inline=False)
|
||||||
|
|
||||||
|
args = MessageArgs(embed=embed)
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
||||||
|
self.instances = tuple(
|
||||||
|
lguild.config.get(setting.setting_id)
|
||||||
|
for setting in self.settings.model_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
async def refresh_components(self):
|
||||||
|
await asyncio.gather(
|
||||||
|
self.category_menu_refresh(),
|
||||||
|
self.visible_button_refresh(),
|
||||||
|
self.edit_button_refresh(),
|
||||||
|
self.close_button_refresh(),
|
||||||
|
self.reset_button_refresh(),
|
||||||
|
)
|
||||||
|
self.set_layout(
|
||||||
|
(self.category_menu,),
|
||||||
|
(self.visible_button, self.edit_button, self.reset_button, self.close_button)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomDashboard(DashboardSection):
|
||||||
|
section_name = _p(
|
||||||
|
'dash:rooms|title',
|
||||||
|
"Private Room Configuration"
|
||||||
|
)
|
||||||
|
configui = RoomSettingUI
|
||||||
|
setting_classes = RoomSettingUI.setting_classes
|
||||||
@@ -27,6 +27,7 @@ class ConfigEditor(FastModal):
|
|||||||
|
|
||||||
class ConfigUI(LeoUI):
|
class ConfigUI(LeoUI):
|
||||||
# TODO: Migrate to a subclass of MessageUI
|
# TODO: Migrate to a subclass of MessageUI
|
||||||
|
# TODO: Move instances to a {setting_id: instance} map for easy retrieval
|
||||||
_listening = {}
|
_listening = {}
|
||||||
setting_classes = []
|
setting_classes = []
|
||||||
|
|
||||||
|
|||||||
@@ -337,6 +337,15 @@ class MessageUI(LeoUI):
|
|||||||
self._original = interaction
|
self._original = interaction
|
||||||
await interaction.response.send_message(**args.send_args, **kwargs, view=self)
|
await interaction.response.send_message(**args.send_args, **kwargs, view=self)
|
||||||
|
|
||||||
|
async def send(self, channel: discord.abc.Messageable, **kwargs):
|
||||||
|
"""
|
||||||
|
Alternative to draw() which uses a discord.abc.Messageable.
|
||||||
|
"""
|
||||||
|
await self.reload()
|
||||||
|
await self.refresh_layout()
|
||||||
|
args = await self.make_message()
|
||||||
|
self._message = await channel.send(**args.send_args, view=self)
|
||||||
|
|
||||||
async def redraw(self, thinking: Optional[discord.Interaction] = None):
|
async def redraw(self, thinking: Optional[discord.Interaction] = None):
|
||||||
"""
|
"""
|
||||||
Update the output message for this UI.
|
Update the output message for this UI.
|
||||||
|
|||||||
Reference in New Issue
Block a user