(rooms): Add interactive lock.
Add an interactive lock so users cannot have multiple menus open simultaneously.
This commit is contained in:
@@ -2,6 +2,7 @@ import re
|
|||||||
import datetime
|
import datetime
|
||||||
import discord
|
import discord
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
from cmdClient.checks import in_guild
|
from cmdClient.checks import in_guild
|
||||||
|
|
||||||
from meta import client
|
from meta import client
|
||||||
@@ -38,6 +39,26 @@ def time_format(time):
|
|||||||
time.timestamp() + 3600,
|
time.timestamp() + 3600,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_locks = {} # Map userid -> ctx
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ensure_exclusive(ctx):
|
||||||
|
"""
|
||||||
|
Cancel any existing exclusive contexts for the author.
|
||||||
|
"""
|
||||||
|
old_ctx = user_locks.pop(ctx.author.id, None)
|
||||||
|
if old_ctx:
|
||||||
|
[task.cancel() for task in old_ctx.tasks]
|
||||||
|
|
||||||
|
user_locks[ctx.author.id] = ctx
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
new_ctx = user_locks.get(ctx.author.id, None)
|
||||||
|
if new_ctx and new_ctx.msg.id == ctx.msg.id:
|
||||||
|
user_locks.pop(ctx.author.id)
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
@module.cmd(
|
||||||
name="rooms",
|
name="rooms",
|
||||||
@@ -101,87 +122,88 @@ async def cmd_rooms(ctx):
|
|||||||
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
try:
|
with ensure_exclusive(ctx):
|
||||||
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
try:
|
try:
|
||||||
await out_msg.edit(
|
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
||||||
content=None,
|
except asyncio.TimeoutError:
|
||||||
embed=discord.Embed(
|
try:
|
||||||
description="Cancel menu timed out, no accountability sessions were cancelled.",
|
await out_msg.edit(
|
||||||
colour=discord.Colour.red()
|
content=None,
|
||||||
|
embed=discord.Embed(
|
||||||
|
description="Cancel menu timed out, no accountability sessions were cancelled.",
|
||||||
|
colour=discord.Colour.red()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
await out_msg.clear_reactions()
|
||||||
await out_msg.clear_reactions()
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await out_msg.delete()
|
||||||
|
await message.delete()
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
if message.content.lower() == 'c':
|
||||||
await out_msg.delete()
|
return
|
||||||
await message.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if message.content.lower() == 'c':
|
to_cancel = [
|
||||||
return
|
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
|
||||||
|
|
||||||
to_cancel = [
|
slotids = [row['slotid'] for row in to_cancel]
|
||||||
joined_rows[index]
|
async with room_lock:
|
||||||
for index in parse_ranges(message.content) if index < len(joined_rows)
|
# TODO: Use the return from this to calculate the cost!
|
||||||
]
|
accountability_members.delete_where(
|
||||||
if not to_cancel:
|
userid=ctx.author.id,
|
||||||
return await ctx.error_reply("No valid bookings selected for cancellation.")
|
slotid=slotids
|
||||||
cost = len(to_cancel) * ctx.guild_settings.accountability_price.value
|
|
||||||
|
|
||||||
slotids = [row['slotid'] for row in to_cancel]
|
|
||||||
async with room_lock:
|
|
||||||
# TODO: Use the return from this to calculate the cost!
|
|
||||||
accountability_members.delete_where(
|
|
||||||
userid=ctx.author.id,
|
|
||||||
slotid=slotids
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle case where the slot has already opened
|
|
||||||
# TODO: Possible race condition if they open over the hour border? Might never cancel
|
|
||||||
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)
|
|
||||||
|
|
||||||
remaining = [row for row in joined_rows if row['slotid'] not in slotids]
|
|
||||||
if not remaining:
|
|
||||||
await ctx.embed_reply("Cancelled all your upcoming accountability sessions!")
|
|
||||||
else:
|
|
||||||
next_booked_time = min(row['start_at'] for row in remaining)
|
|
||||||
if len(to_cancel) > 1:
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"Cancelled `{}` upcoming sessions!\nYour next session is at <t:{:.0f}>.".format(
|
|
||||||
len(to_cancel),
|
|
||||||
next_booked_time.timestamp()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Handle case where the slot has already opened
|
||||||
|
# TODO: Possible race condition if they open over the hour border? Might never cancel
|
||||||
|
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)
|
||||||
|
|
||||||
|
remaining = [row for row in joined_rows if row['slotid'] not in slotids]
|
||||||
|
if not remaining:
|
||||||
|
await ctx.embed_reply("Cancelled all your upcoming accountability sessions!")
|
||||||
else:
|
else:
|
||||||
await ctx.embed_reply(
|
next_booked_time = min(row['start_at'] for row in remaining)
|
||||||
"Cancelled your session at <t:{:.0f}>!\n"
|
if len(to_cancel) > 1:
|
||||||
"Your next session is at <t:{:.0f}>.".format(
|
await ctx.embed_reply(
|
||||||
to_cancel[0]['start_at'].timestamp(),
|
"Cancelled `{}` upcoming sessions!\nYour next session is at <t:{:.0f}>.".format(
|
||||||
next_booked_time.timestamp()
|
len(to_cancel),
|
||||||
|
next_booked_time.timestamp()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.embed_reply(
|
||||||
|
"Cancelled your session at <t:{:.0f}>!\n"
|
||||||
|
"Your next session is at <t:{:.0f}>.".format(
|
||||||
|
to_cancel[0]['start_at'].timestamp(),
|
||||||
|
next_booked_time.timestamp()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
elif command == 'book':
|
elif command == 'book':
|
||||||
# Show booking menu
|
# Show booking menu
|
||||||
# Get attendee count
|
# Get attendee count
|
||||||
@@ -239,86 +261,87 @@ async def cmd_rooms(ctx):
|
|||||||
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
try:
|
with ensure_exclusive(ctx):
|
||||||
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
try:
|
try:
|
||||||
await out_msg.edit(
|
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
||||||
content=None,
|
except asyncio.TimeoutError:
|
||||||
embed=discord.Embed(
|
try:
|
||||||
description="Booking menu timed out, no sessions were booked.",
|
await out_msg.edit(
|
||||||
colour=discord.Colour.red()
|
content=None,
|
||||||
|
embed=discord.Embed(
|
||||||
|
description="Booking menu timed out, no sessions were booked.",
|
||||||
|
colour=discord.Colour.red()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
await out_msg.clear_reactions()
|
||||||
await out_msg.clear_reactions()
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await out_msg.delete()
|
||||||
|
await message.delete()
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
if message.content.lower() == 'c':
|
||||||
await out_msg.delete()
|
return
|
||||||
await message.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if message.content.lower() == 'c':
|
to_book = [
|
||||||
return
|
times[index]
|
||||||
|
for index in parse_ranges(message.content) if index < len(times)
|
||||||
to_book = [
|
]
|
||||||
times[index]
|
if not to_book:
|
||||||
for index in parse_ranges(message.content) if index < len(times)
|
return await ctx.error_reply("No valid sessions selected.")
|
||||||
]
|
cost = len(to_book) * ctx.guild_settings.accountability_price.value
|
||||||
if not to_book:
|
if cost > ctx.alion.coins:
|
||||||
return await ctx.error_reply("No valid sessions selected.")
|
return await ctx.error_reply(
|
||||||
cost = len(to_book) * ctx.guild_settings.accountability_price.value
|
"Sorry, booking `{}` sessions costs `{}` coins, and you only have `{}`!".format(
|
||||||
if cost > ctx.alion.coins:
|
len(to_book),
|
||||||
return await ctx.error_reply(
|
cost,
|
||||||
"Sorry, booking `{}` sessions costs `{}` coins, and you only have `{}`!".format(
|
ctx.alion.coins
|
||||||
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')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the member to data, creating the row if required
|
# Handle case where the slot has already opened
|
||||||
slot_rows = accountability_rooms.fetch_rows_where(
|
aguild = AGuild.cache.get(ctx.guild.id, None)
|
||||||
guildid=ctx.guild.id,
|
if aguild:
|
||||||
start_at=to_book
|
if aguild.upcoming_slot and aguild.upcoming_slot.start_time in to_book:
|
||||||
)
|
slot = aguild.upcoming_slot
|
||||||
slotids = [row.slotid for row in slot_rows]
|
if not slot.data:
|
||||||
to_add = set(to_book).difference((row.start_at for row in slot_rows))
|
# Handle slot activation
|
||||||
if to_add:
|
slot._refresh()
|
||||||
slotids.extend(row['slotid'] for row in accountability_rooms.insert_many(
|
channelid, messageid = await slot.open()
|
||||||
*((ctx.guild.id, start_at) for start_at in to_add),
|
accountability_rooms.update_where(
|
||||||
insert_keys=('guildid', 'start_at'),
|
{'channelid': channelid, 'messageid': messageid},
|
||||||
))
|
slotid=slot.data.slotid
|
||||||
accountability_members.insert_many(
|
)
|
||||||
*((slotid, ctx.author.id, ctx.guild_settings.accountability_price.value) for slotid in slotids),
|
else:
|
||||||
insert_keys=('slotid', 'userid', 'paid')
|
slot.members[ctx.author.id] = SlotMember(slot.data.slotid, ctx.author.id, ctx.guild)
|
||||||
)
|
# Also update the channel permissions
|
||||||
|
try:
|
||||||
# Handle case where the slot has already opened
|
await slot.channel.set_permissions(ctx.author, view_channel=True, connect=True)
|
||||||
aguild = AGuild.cache.get(ctx.guild.id, None)
|
except discord.HTTPException:
|
||||||
if aguild:
|
pass
|
||||||
if aguild.upcoming_slot and aguild.upcoming_slot.start_time in to_book:
|
await slot.update_status()
|
||||||
slot = aguild.upcoming_slot
|
ctx.alion.addCoins(-cost)
|
||||||
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)
|
|
||||||
|
|
||||||
# Ack purchase
|
# Ack purchase
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
|
|||||||
Reference in New Issue
Block a user