Add locking to room init, turnover, and cancellation. Add cleanup of nonexistent members in slot init. Fix an issue where members were being charged for cancelling rooms.
281 lines
10 KiB
Python
281 lines
10 KiB
Python
import re
|
|
import datetime
|
|
import discord
|
|
import asyncio
|
|
from cmdClient.checks import in_guild
|
|
|
|
from utils.lib import multiselect_regex, parse_ranges
|
|
from data import NOTNULL
|
|
from data.conditions import GEQ
|
|
|
|
from .module import module
|
|
from .lib import utc_now
|
|
from .tracker import AccountabilityGuild as AGuild
|
|
from .tracker import room_lock
|
|
from .TimeSlot import SlotMember
|
|
from .data import accountability_members, accountability_member_info, accountability_rooms
|
|
|
|
|
|
@module.cmd(
|
|
name="rooms",
|
|
desc="Book an accountability timeslot",
|
|
group="Productivity"
|
|
)
|
|
@in_guild()
|
|
async def cmd_rooms(ctx):
|
|
"""
|
|
Usage``:
|
|
{prefix}rooms
|
|
{prefix}rooms book
|
|
{prefix}rooms cancel
|
|
Description:
|
|
Book an accountability session timeslot.
|
|
"""
|
|
lower = ctx.args.lower()
|
|
splits = lower.split()
|
|
command = splits[0] if splits else None
|
|
|
|
if not ctx.guild_settings.accountability_category.value:
|
|
return await ctx.error_reply("The accountability system isn't set up!")
|
|
|
|
# First grab the sessions the member is booked in
|
|
joined_rows = accountability_member_info.select_where(
|
|
userid=ctx.author.id,
|
|
start_at=GEQ(utc_now()),
|
|
_extra="ORDER BY start_at ASC"
|
|
)
|
|
|
|
if command == 'cancel':
|
|
if not joined_rows:
|
|
return await ctx.error_reply("You have no bookings to cancel!")
|
|
|
|
# Show unbooking menu
|
|
lines = [
|
|
"`[{:>2}]` | <t:{}:d> <t:{}:t> - <t:{}:t>".format(
|
|
i,
|
|
int(row['start_at'].timestamp()),
|
|
int(row['start_at'].timestamp()),
|
|
int(row['start_at'].timestamp()) + 3600
|
|
)
|
|
for i, row in enumerate(joined_rows)
|
|
]
|
|
out_msg = await ctx.reply(
|
|
embed=discord.Embed(
|
|
title="Please choose the bookings you want to cancel.",
|
|
description='\n'.join(lines),
|
|
colour=discord.Colour.orange()
|
|
).set_footer(
|
|
text=(
|
|
"Reply with the number(s) of the rooms you want to join.\n"
|
|
"E.g. 1, 3, 5 or 1-5, 7-8."
|
|
)
|
|
)
|
|
)
|
|
|
|
def check(msg):
|
|
valid = msg.channel == ctx.ch and msg.author == ctx.author
|
|
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
|
return valid
|
|
|
|
try:
|
|
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
|
except asyncio.TimeoutError:
|
|
try:
|
|
await out_msg.delete()
|
|
await ctx.error_reply("Session timed out. No accountability bookings were cancelled.")
|
|
except discord.HTTPException:
|
|
pass
|
|
return
|
|
|
|
try:
|
|
await out_msg.delete()
|
|
await message.delete()
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
if message.content.lower() == 'c':
|
|
return
|
|
|
|
to_cancel = [
|
|
joined_rows[index]
|
|
for index in parse_ranges(message.content) if index < len(joined_rows)
|
|
]
|
|
if not to_cancel:
|
|
return await ctx.error_reply("No valid bookings selected for cancellation.")
|
|
cost = len(to_cancel) * ctx.guild_settings.accountability_price.value
|
|
|
|
slotids = [row['slotid'] for row in to_cancel]
|
|
async with room_lock:
|
|
accountability_members.delete_where(
|
|
userid=ctx.author.id,
|
|
slotid=slotids
|
|
)
|
|
|
|
# Handle case where the slot has already opened
|
|
for row in to_cancel:
|
|
aguild = AGuild.cache.get(row['guildid'], None)
|
|
if aguild and aguild.upcoming_slot and aguild.upcoming_slot.data:
|
|
if aguild.upcoming_slot.data.slotid in slotids:
|
|
aguild.upcoming_slot.members.pop(ctx.author.id, None)
|
|
if aguild.upcoming_slot.channel:
|
|
try:
|
|
await aguild.upcoming_slot.channel.set_permissions(
|
|
ctx.author,
|
|
overwrite=None
|
|
)
|
|
except discord.HTTPException:
|
|
pass
|
|
await aguild.upcoming_slot.update_status()
|
|
break
|
|
|
|
ctx.alion.addCoins(cost)
|
|
await ctx.embed_reply(
|
|
"Successfully canceled your bookings."
|
|
)
|
|
# elif command == 'book':
|
|
else:
|
|
# Show booking menu
|
|
|
|
# Get attendee count
|
|
rows = accountability_member_info.select_where(
|
|
guildid=ctx.guild.id,
|
|
userid=NOTNULL,
|
|
select_columns=(
|
|
'slotid',
|
|
'start_at',
|
|
'COUNT(*) as num'
|
|
),
|
|
_extra="GROUP BY start_at, slotid"
|
|
)
|
|
attendees = {row['start_at']: row['num'] for row in rows}
|
|
attendee_pad = max((len(str(num)) for num in attendees.values()), default=1)
|
|
|
|
# Build lines
|
|
already_joined_times = set(row['start_at'] for row in joined_rows)
|
|
start_time = utc_now().replace(minute=0, second=0, microsecond=0)
|
|
times = (
|
|
start_time + datetime.timedelta(hours=n)
|
|
for n in range(1, 25)
|
|
)
|
|
times = list(time for time in times if time not in already_joined_times)
|
|
lines = [
|
|
"`[{:>2}]` | `{:>{}}` attending | <t:{}:d> <t:{}:t> - <t:{}:t>".format(
|
|
i,
|
|
attendees.get(time, 0), attendee_pad,
|
|
int(time.timestamp()), int(time.timestamp()), int(time.timestamp()) + 3600
|
|
)
|
|
for i, time in enumerate(times)
|
|
]
|
|
# TODO: Nicer embed
|
|
# TODO: Don't allow multi bookings if the member has a bad attendence rate
|
|
out_msg = await ctx.reply(
|
|
embed=discord.Embed(
|
|
title="Please choose the sessions you want to book.",
|
|
description='\n'.join(lines),
|
|
colour=discord.Colour.orange()
|
|
).set_footer(
|
|
text=(
|
|
"Reply with the number(s) of the rooms you want to join.\n"
|
|
"E.g. 1, 3, 5 or 1-5, 7-8."
|
|
)
|
|
)
|
|
)
|
|
|
|
def check(msg):
|
|
valid = msg.channel == ctx.ch and msg.author == ctx.author
|
|
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
|
return valid
|
|
|
|
try:
|
|
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
|
except asyncio.TimeoutError:
|
|
try:
|
|
await out_msg.delete()
|
|
await ctx.error_reply("Session timed out. No accountability slots were booked.")
|
|
except discord.HTTPException:
|
|
pass
|
|
return
|
|
|
|
try:
|
|
await out_msg.delete()
|
|
await message.delete()
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
if message.content.lower() == 'c':
|
|
return
|
|
|
|
to_book = [
|
|
times[index]
|
|
for index in parse_ranges(message.content) if index < len(times)
|
|
]
|
|
if not to_book:
|
|
return await ctx.error_reply("No valid sessions selected.")
|
|
cost = len(to_book) * ctx.guild_settings.accountability_price.value
|
|
if cost > ctx.alion.coins:
|
|
return await ctx.error_reply(
|
|
"Sorry, booking `{}` sessions costs `{}` coins, and you only have `{}`!".format(
|
|
len(to_book),
|
|
cost,
|
|
ctx.alion.coins
|
|
)
|
|
)
|
|
|
|
# Add the member to data, creating the row if required
|
|
slot_rows = accountability_rooms.fetch_rows_where(
|
|
guildid=ctx.guild.id,
|
|
start_at=to_book
|
|
)
|
|
slotids = [row.slotid for row in slot_rows]
|
|
to_add = set(to_book).difference((row.start_at for row in slot_rows))
|
|
if to_add:
|
|
slotids.extend(row['slotid'] for row in accountability_rooms.insert_many(
|
|
*((ctx.guild.id, start_at) for start_at in to_add),
|
|
insert_keys=('guildid', 'start_at'),
|
|
))
|
|
accountability_members.insert_many(
|
|
*((slotid, ctx.author.id, ctx.guild_settings.accountability_price.value) for slotid in slotids),
|
|
insert_keys=('slotid', 'userid', 'paid')
|
|
)
|
|
|
|
# Handle case where the slot has already opened
|
|
aguild = AGuild.cache.get(ctx.guild.id, None)
|
|
if aguild:
|
|
if aguild.upcoming_slot and aguild.upcoming_slot.start_time in to_book:
|
|
slot = aguild.upcoming_slot
|
|
if not slot.data:
|
|
# Handle slot activation
|
|
slot._refresh()
|
|
channelid, messageid = await slot.open()
|
|
accountability_rooms.update_where(
|
|
{'channelid': channelid, 'messageid': messageid},
|
|
slotid=slot.data.slotid
|
|
)
|
|
else:
|
|
slot.members[ctx.author.id] = SlotMember(slot.data.slotid, ctx.author.id, ctx.guild)
|
|
# Also update the channel permissions
|
|
try:
|
|
await slot.channel.set_permissions(ctx.author, view_channel=True, connect=True)
|
|
except discord.HTTPException:
|
|
pass
|
|
await slot.update_status()
|
|
ctx.alion.addCoins(-cost)
|
|
await ctx.embed_reply(
|
|
"You have booked the following accountability sessions.\n{}".format(
|
|
'\n'.join(
|
|
"<t:{}:d> <t:{}:t> - <t:{}:t>".format(
|
|
int(time.timestamp()), int(time.timestamp()), int(time.timestamp() + 3600)
|
|
) for time in to_book
|
|
)
|
|
)
|
|
)
|
|
# else:
|
|
# # Show current booking information
|
|
# embed = discord.Embed(
|
|
# title="Accountability Room Information"
|
|
# )
|
|
# ...
|
|
|
|
|
|
# TODO: roomadmin
|