rewrite: New private Room system.
This commit is contained in:
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())
|
||||
Reference in New Issue
Block a user