(Renting): Created the Renting module and system.
This commit is contained in:
@@ -7,4 +7,5 @@ from .user_config import *
|
||||
from .workout import *
|
||||
from .todo import *
|
||||
from .reminders import *
|
||||
from .renting import *
|
||||
# from .moderation import *
|
||||
|
||||
5
bot/modules/renting/__init__.py
Normal file
5
bot/modules/renting/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .module import module
|
||||
|
||||
from . import commands
|
||||
from . import rooms
|
||||
from . import admin
|
||||
76
bot/modules/renting/admin.py
Normal file
76
bot/modules/renting/admin.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import discord
|
||||
|
||||
from settings import GuildSettings, GuildSetting
|
||||
import settings
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class rent_category(settings.Channel, GuildSetting):
|
||||
category = "Rented Rooms"
|
||||
|
||||
attr_name = "rent_category"
|
||||
_data_column = "renting_category"
|
||||
|
||||
display_name = "rent_category"
|
||||
desc = "Category in which members can rent their own study rooms."
|
||||
|
||||
_default = None
|
||||
|
||||
long_desc = (
|
||||
"Members can use the `rent` command to "
|
||||
"buy the use of a new private voice channel in this category for `24h`."
|
||||
)
|
||||
_accepts = "A category channel."
|
||||
|
||||
_chan_type = discord.ChannelType.category
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
if self.value:
|
||||
return "Members may now rent private voice channels under **{}**.".format(self.value.name)
|
||||
else:
|
||||
return "Members may no longer rent private voice channels."
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class rent_member_limit(settings.Integer, GuildSetting):
|
||||
category = "Rented Rooms"
|
||||
|
||||
attr_name = "rent_member_limit"
|
||||
_data_column = "renting_cap"
|
||||
|
||||
display_name = "rent_member_limit"
|
||||
desc = "Maximum number of people that can be added to a rented room."
|
||||
|
||||
_default = 10
|
||||
|
||||
long_desc = (
|
||||
"Maximum number of people a member can add to a rented private voice channel."
|
||||
)
|
||||
_accepts = "An integer number of members."
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
return "Members will now be able to add at most `{}` people to their channel.".format(self.value)
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class rent_room_price(settings.Integer, GuildSetting):
|
||||
category = "Rented Rooms"
|
||||
|
||||
attr_name = "rent_room_price"
|
||||
_data_column = "renting_price"
|
||||
|
||||
display_name = "rent_price"
|
||||
desc = "Price of a privated voice channel."
|
||||
|
||||
_default = 1000
|
||||
|
||||
long_desc = (
|
||||
"How much it costs for a member to rent a private voice channel."
|
||||
)
|
||||
_accepts = "An integer number of coins."
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
return "Private voice channels now cost `{}` coins.".format(self.value)
|
||||
202
bot/modules/renting/commands.py
Normal file
202
bot/modules/renting/commands.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import discord
|
||||
from cmdClient.checks import in_guild
|
||||
|
||||
from .module import module
|
||||
from .rooms import Room
|
||||
|
||||
|
||||
@module.cmd(
|
||||
name="rent",
|
||||
desc="Rent a private study room!",
|
||||
group="Productivity",
|
||||
aliases=('add',)
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_rent(ctx):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}rent
|
||||
{prefix}rent @user1 @user2 @user3 ...
|
||||
{prefix}rent add @user1 @user2 @user3 ...
|
||||
{prefix}rent remove @user1 @user2 @user3 ...
|
||||
Description:
|
||||
Rent a private voice channel for 24 hours,\
|
||||
and invite up to `{ctx.guild_settings.rent_member_limit.value}` mentioned users.
|
||||
Use `{prefix}rent add` and `{prefix}rent remove` to give/revoke access to your room.
|
||||
|
||||
*Renting a private channel costs `{ctx.guild_settings.rent_room_price.value} LC`.*
|
||||
"""
|
||||
# TODO: More gracefully handle unexpected channel deletion
|
||||
|
||||
# Check if the category is set up
|
||||
if not ctx.guild_settings.rent_category.value:
|
||||
return await ctx.error_reply(
|
||||
"The private study channel category has not been set up! Please come back later."
|
||||
)
|
||||
|
||||
# Fetch the members' room, if it exists
|
||||
room = Room.fetch(ctx.guild.id, ctx.author.id)
|
||||
|
||||
if room:
|
||||
# Show room status, or add/remove remebers
|
||||
lower = ctx.args.lower()
|
||||
if ctx.msg.mentions and lower and (lower.startswith('-') or lower.startswith('remove')):
|
||||
# Remove the mentioned members
|
||||
|
||||
# Extract members to remove
|
||||
current_memberids = set(room.memberids)
|
||||
to_remove = (
|
||||
member for member in ctx.msg.mentions
|
||||
if member.id in current_memberids
|
||||
)
|
||||
to_remove = list(set(to_remove)) # Remove duplicates
|
||||
|
||||
# Check if there are no members to remove
|
||||
if not to_remove:
|
||||
return await ctx.error_reply(
|
||||
"None of these members have access to your study room!"
|
||||
)
|
||||
|
||||
# Finally, remove the members from the room and ack
|
||||
await room.remove_members(*to_remove)
|
||||
|
||||
await ctx.embed_reply(
|
||||
"The following members have been removed from your room:\n{}".format(
|
||||
', '.join(member.mention for member in to_remove)
|
||||
)
|
||||
)
|
||||
elif lower == 'delete':
|
||||
if await ctx.ask("Are you sure you want to delete your study room? No refunds given!"):
|
||||
# TODO: Better deletion log
|
||||
await room._execute()
|
||||
await ctx.embed_reply("Private study room deleted.")
|
||||
elif ctx.msg.mentions:
|
||||
# Add the mentioned members
|
||||
|
||||
# Extract members to add
|
||||
current_memberids = set(room.memberids)
|
||||
to_add = (
|
||||
member for member in ctx.msg.mentions
|
||||
if member.id not in current_memberids and member.id != ctx.author
|
||||
)
|
||||
to_add = list(set(to_add)) # Remove duplicates
|
||||
|
||||
# Check if there are no members to add
|
||||
if not to_add:
|
||||
return await ctx.error_reply(
|
||||
"All of these members already have access to your room!"
|
||||
)
|
||||
|
||||
# Check that they didn't provide too many members
|
||||
limit = ctx.guild_settings.rent_member_limit.value
|
||||
if len(to_add) + len(current_memberids) > limit:
|
||||
return await ctx.error_reply(
|
||||
"Too many members! You can invite at most `{}` members to your room.".format(
|
||||
limit
|
||||
)
|
||||
)
|
||||
|
||||
# Finally, add the members to the room and ack
|
||||
await room.add_members(*to_add)
|
||||
|
||||
await ctx.embed_reply(
|
||||
"The following members have been given access to your room:\n{}".format(
|
||||
', '.join(member.mention for member in to_add)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Show room status with hints for adding and removing members
|
||||
# Ack command
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange()
|
||||
).set_author(
|
||||
name="{}'s private room".format(ctx.author.display_name),
|
||||
icon_url=ctx.author.avatar_url
|
||||
).add_field(
|
||||
name="Channel",
|
||||
value=room.channel.mention
|
||||
).add_field(
|
||||
name="Expires",
|
||||
value="<t:{}:R>".format(room.timestamp)
|
||||
).add_field(
|
||||
name="Members",
|
||||
value=', '.join('<@{}>'.format(memberid) for memberid in room.memberids) or "None",
|
||||
inline=False
|
||||
).set_footer(
|
||||
text=(
|
||||
"Use '{prefix}rent add @mention' and '{prefix}rent remove @mention'\n"
|
||||
"to add and remove members.".format(prefix=ctx.best_prefix)
|
||||
),
|
||||
icon_url="https://projects.iamcal.com/emoji-data/img-apple-64/1f4a1.png"
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
else:
|
||||
if ctx.args:
|
||||
# Rent a new room
|
||||
|
||||
# Check that they provided at least one member
|
||||
if not ctx.msg.mentions:
|
||||
return await ctx.error_reply(
|
||||
"Please mention at least one user to add to your new room."
|
||||
)
|
||||
|
||||
to_add = (
|
||||
member for member in ctx.msg.mentions if member != ctx.author
|
||||
)
|
||||
to_add = list(set(to_add))
|
||||
|
||||
# Check that they didn't provide too many members
|
||||
limit = ctx.guild_settings.rent_member_limit.value
|
||||
if len(ctx.msg.mentions) > limit:
|
||||
return await ctx.error_reply(
|
||||
"Too many members! You can invite at most `{}` members to your room.".format(
|
||||
limit
|
||||
)
|
||||
)
|
||||
|
||||
# Check that they have enough money for this
|
||||
cost = ctx.guild_settings.rent_room_price.value
|
||||
if ctx.alion.coins < cost:
|
||||
return await ctx.error_reply(
|
||||
"Sorry, a private room costs `{}` coins, but you only have `{}`.".format(
|
||||
cost,
|
||||
ctx.alion.coins
|
||||
)
|
||||
)
|
||||
|
||||
# Create the room
|
||||
room = await Room.create(ctx.author, to_add)
|
||||
|
||||
# Deduct cost
|
||||
ctx.alion.addCoins(-cost)
|
||||
|
||||
# Ack command
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title="Private study room rented!",
|
||||
).add_field(
|
||||
name="Channel",
|
||||
value=room.channel.mention
|
||||
).add_field(
|
||||
name="Expires",
|
||||
value="<t:{}:R>".format(room.timestamp)
|
||||
).add_field(
|
||||
name="Members",
|
||||
value=', '.join(member.mention for member in to_add),
|
||||
inline=False
|
||||
).set_footer(
|
||||
text="See your room status at any time with {prefix}rent".format(prefix=ctx.best_prefix),
|
||||
icon_url="https://projects.iamcal.com/emoji-data/img-apple-64/1f4a1.png"
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
else:
|
||||
# Suggest they get a room
|
||||
await ctx.embed_reply(
|
||||
"Rent a private study room for 24 hours with up to `{}` "
|
||||
"friends by mentioning them with this command! (Rooms cost `{}` LionCoins.)\n"
|
||||
"`{}rent @user1 @user2 ...`".format(
|
||||
ctx.guild_settings.rent_member_limit.value,
|
||||
ctx.best_prefix,
|
||||
ctx.guild_settings.rent_room_price.value,
|
||||
)
|
||||
)
|
||||
11
bot/modules/renting/data.py
Normal file
11
bot/modules/renting/data.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from data import RowTable, Table
|
||||
|
||||
|
||||
rented = RowTable(
|
||||
'rented',
|
||||
('channelid', 'guildid', 'ownerid', 'expires_at', 'created_at'),
|
||||
'channelid'
|
||||
)
|
||||
|
||||
|
||||
rented_members = Table('rented_members')
|
||||
4
bot/modules/renting/module.py
Normal file
4
bot/modules/renting/module.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from LionModule import LionModule
|
||||
|
||||
|
||||
module = LionModule("Rented Rooms")
|
||||
283
bot/modules/renting/rooms.py
Normal file
283
bot/modules/renting/rooms.py
Normal file
@@ -0,0 +1,283 @@
|
||||
import discord
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
from cmdClient.lib import SafeCancellation
|
||||
|
||||
from meta import client
|
||||
from settings import GuildSettings
|
||||
|
||||
from .data import rented, rented_members
|
||||
from .module import module
|
||||
|
||||
|
||||
class Room:
|
||||
__slots__ = ('key', 'map_key', '_task')
|
||||
|
||||
everyone_overwrite = discord.PermissionOverwrite(
|
||||
view_channel=False
|
||||
)
|
||||
owner_overwrite = discord.PermissionOverwrite(
|
||||
view_channel=True,
|
||||
connect=True,
|
||||
priority_speaker=True
|
||||
)
|
||||
member_overwrite = discord.PermissionOverwrite(
|
||||
view_channel=True,
|
||||
connect=True,
|
||||
)
|
||||
|
||||
_table = rented
|
||||
|
||||
_rooms = {} # map (guildid, userid) -> Room
|
||||
|
||||
def __init__(self, channelid):
|
||||
self.key = channelid
|
||||
self.map_key = (self.data.guildid, self.data.ownerid)
|
||||
|
||||
self._task = None
|
||||
|
||||
@classmethod
|
||||
async def create(cls, owner: discord.Member, initial_members):
|
||||
ownerid = owner.id
|
||||
guild = owner.guild
|
||||
guildid = guild.id
|
||||
guild_settings = GuildSettings(guildid)
|
||||
|
||||
category = guild_settings.rent_category.value
|
||||
if not category:
|
||||
# This should never happen
|
||||
return SafeCancellation("Rent category not set up!")
|
||||
|
||||
# First create the channel, with the needed overrides
|
||||
overwrites = {
|
||||
guild.default_role: cls.everyone_overwrite,
|
||||
owner: cls.owner_overwrite
|
||||
}
|
||||
overwrites.update(
|
||||
{member: cls.member_overwrite for member in initial_members}
|
||||
)
|
||||
try:
|
||||
channel = await guild.create_voice_channel(
|
||||
name="{}'s private channel".format(owner.name),
|
||||
overwrites=overwrites,
|
||||
category=category
|
||||
)
|
||||
channelid = channel.id
|
||||
except discord.HTTPException:
|
||||
guild_settings.event_log.log(
|
||||
description="Failed to create a private room for {}!".format(owner.mention),
|
||||
colour=discord.Colour.red()
|
||||
)
|
||||
raise SafeCancellation("Couldn't create the private channel! Please try again later.")
|
||||
|
||||
# Add the new room to data
|
||||
cls._table.create_row(
|
||||
channelid=channelid,
|
||||
guildid=guildid,
|
||||
ownerid=ownerid
|
||||
)
|
||||
|
||||
# Add the members to data, if any
|
||||
if initial_members:
|
||||
rented_members.insert_many(
|
||||
*((channelid, member.id) for member in initial_members)
|
||||
)
|
||||
|
||||
# Log the creation
|
||||
guild_settings.event_log.log(
|
||||
title="New private study room!",
|
||||
description="Created a private study room for {} with:\n{}".format(
|
||||
owner.mention,
|
||||
', '.join(member.mention for member in initial_members)
|
||||
)
|
||||
)
|
||||
|
||||
# Create the room, schedule its expiry, and return
|
||||
room = cls(channelid)
|
||||
room.schedule()
|
||||
return room
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, guildid, userid):
|
||||
"""
|
||||
Fetch a Room owned by a given member.
|
||||
"""
|
||||
return cls._rooms.get((guildid, userid), None)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._table.fetch(self.key)
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""
|
||||
The Member owning the room, if we can find them
|
||||
"""
|
||||
guild = client.get_guild(self.data.guildid)
|
||||
if guild:
|
||||
return guild.get_member(self.data.ownerid)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""
|
||||
The Channel corresponding to this rented room.
|
||||
"""
|
||||
guild = client.get_guild(self.data.guildid)
|
||||
if guild:
|
||||
return guild.get_channel(self.key)
|
||||
|
||||
@property
|
||||
def memberids(self):
|
||||
"""
|
||||
The list of memberids in the channel.
|
||||
"""
|
||||
return [row['userid'] for row in rented_members.select_where(channelid=self.key)]
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
"""
|
||||
True unix timestamp for the room expiry time.
|
||||
"""
|
||||
return int(self.data.expires_at.replace(tzinfo=datetime.timezone.utc).timestamp())
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete the room in an idempotent way.
|
||||
"""
|
||||
if self._task and not self._task.done():
|
||||
self._task.cancel()
|
||||
self._rooms.pop(self.map_key, None)
|
||||
self._table.delete_where(channelid=self.key)
|
||||
|
||||
def schedule(self):
|
||||
"""
|
||||
Schedule this room to be expired.
|
||||
"""
|
||||
asyncio.create_task(self._schedule())
|
||||
self._rooms[self.map_key] = self
|
||||
|
||||
async def _schedule(self):
|
||||
"""
|
||||
Expire the room after a sleep period.
|
||||
"""
|
||||
# Calculate time left
|
||||
remaining = (self.data.expires_at - datetime.datetime.utcnow()).total_seconds()
|
||||
|
||||
# Create the waiting task and wait for it, accepting cancellation
|
||||
self._task = asyncio.create_task(asyncio.sleep(remaining))
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
await self._execute()
|
||||
|
||||
async def _execute(self):
|
||||
"""
|
||||
Expire the room.
|
||||
"""
|
||||
owner = self.owner
|
||||
guild_settings = GuildSettings(owner.guild.id)
|
||||
|
||||
if self.channel:
|
||||
# Delete the discord channel
|
||||
try:
|
||||
await self.channel.delete()
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
# Delete the room from data (cascades to member deletion)
|
||||
self.delete()
|
||||
|
||||
guild_settings.event_log.log(
|
||||
title="Private study room expired!",
|
||||
description="{}'s private study room expired.".format(owner.mention)
|
||||
)
|
||||
|
||||
async def add_members(self, *members):
|
||||
guild_settings = GuildSettings(self.data.guildid)
|
||||
|
||||
# Update overwrites
|
||||
new_overwrites = {member: self.member_overwrite for member in members}
|
||||
try:
|
||||
await self.channel.edit(overwrites=new_overwrites)
|
||||
except discord.HTTPException:
|
||||
guild_settings.event_log.log(
|
||||
title="Failed to update study room permissions!",
|
||||
description="An error occured while adding the following users to the private room {}.\n{}".format(
|
||||
self.channel.mention,
|
||||
', '.join(member.mention for member in members)
|
||||
),
|
||||
colour=discord.Colour.red()
|
||||
)
|
||||
raise SafeCancellation("Sorry, something went wrong while adding the members!")
|
||||
|
||||
# Update data
|
||||
rented_members.insert_many(
|
||||
*((self.key, member.id) for member in members)
|
||||
)
|
||||
|
||||
# Log
|
||||
guild_settings.event_log.log(
|
||||
title="New members added to private study room",
|
||||
description="The following were added to {}.\n{}".format(
|
||||
self.channel.mention,
|
||||
', '.join(member.mention for member in members)
|
||||
)
|
||||
)
|
||||
|
||||
async def remove_members(self, *members):
|
||||
guild_settings = GuildSettings(self.data.guildid)
|
||||
|
||||
if self.channel:
|
||||
# Update overwrites
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(self.channel.set_permissions(
|
||||
member,
|
||||
overwrite=None,
|
||||
reason="Removing members from private channel.") for member in members)
|
||||
)
|
||||
except discord.HTTPException:
|
||||
guild_settings.event_log.log(
|
||||
title="Failed to update study room permissions!",
|
||||
description=("An error occured while removing the "
|
||||
"following members from the private room {}.\n{}").format(
|
||||
self.channel.mention,
|
||||
', '.join(member.mention for member in members)
|
||||
),
|
||||
colour=discord.Colour.red()
|
||||
)
|
||||
raise SafeCancellation("Sorry, something went wrong while removing those members!")
|
||||
|
||||
# Disconnect members if possible:
|
||||
to_disconnect = set(self.channel.members).intersection(members)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(member.edit(voice_channel=None) for member in to_disconnect)
|
||||
)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
# Update data
|
||||
rented_members.delete_where(channelid=self.key, userid=[member.id for member in members])
|
||||
|
||||
# Log
|
||||
guild_settings.event_log.log(
|
||||
title="Members removed from a private study room",
|
||||
description="The following were removed from {}.\n{}".format(
|
||||
self.channel.mention if self.channel else "`{}`".format(self.key),
|
||||
', '.join(member.mention for member in members)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@module.launch_task
|
||||
async def load_rented_rooms(client):
|
||||
rows = rented.fetch_rows_where()
|
||||
for row in rows:
|
||||
Room(row.channelid).schedule()
|
||||
client.log(
|
||||
"Loaded {} private study channels.".format(len(rows)),
|
||||
context="LOAD_RENTED_ROOMS"
|
||||
)
|
||||
@@ -45,7 +45,12 @@ CREATE TABLE guild_config(
|
||||
task_reward INTEGER,
|
||||
task_reward_limit INTEGER,
|
||||
study_hourly_reward INTEGER,
|
||||
study_hourly_live_bonus INTEGER
|
||||
study_hourly_live_bonus INTEGER,
|
||||
renting_price INTEGER,
|
||||
renting_category BIGINT,
|
||||
renting_cap INTEGER,
|
||||
renting_role BIGINT,
|
||||
renting_sync_perms BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE unranked_roles(
|
||||
@@ -296,4 +301,21 @@ CREATE VIEW new_study_badges AS
|
||||
ORDER BY guildid;
|
||||
-- }}}
|
||||
|
||||
-- Rented Room data {{{
|
||||
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);
|
||||
-- }}}
|
||||
-- vim: set fdm=marker:
|
||||
|
||||
Reference in New Issue
Block a user