async def update_template_field_delete(request: Request): """ The route that handles the updating of the template metadata. """ # Make sure the user is logged in session = await aiohttp_session.get_session(request) if not session.get("user_id"): return json_response({"error": "Not logged in."}, status=401) # Try and read the POST data from the user try: data = await request.json() field_id = data['field_id'] except ValueError: return json_response({"error": "Couldn't validate the given POST data."}, status=400) except Exception: return json_response({"error": "Could not read POST data."}, status=400) # Now we want to check things with the database async with request.app['database']() as db: # Make sure the template exists field_rows = await db("""SELECT * FROM field WHERE field_id=$1""", field_id) if not field_rows: return json_response({"error": "Field does not exist."}, status=400) field = localutils.Field(**field_rows[0]) template = await localutils.Template.fetch_template_by_id(db, field.template_id, fetch_fields=False) if not template: return json_response({"error": "Template does not exist."}, status=400) # Make sure the user is editing a template that they have permission to edit try: guild = await request.app['bots']['bot'].fetch_guild(template.guild_id) except discord.HTTPException: return json_response({"error": "Bot not in guild."}, status=401) try: member = await guild.fetch_member(session.get("user_id")) except discord.HTTPException: return json_response({"error": "User not in guild."}, status=401) if not member.guild_permissions.manage_guild: return json_response({"error": "Member cannot manage guild."}, status=401) # Update the template updated_rows = await db( """UPDATE field SET deleted=true WHERE field_id=$1""", field_id, ) # Return return json_response({"error": ""}, status=200)
async def create_new_field( self, ctx: utils.Context, template: utils.Template, index: int, image_set: bool = False, prompt_for_creation: bool = True, delete_messages: bool = False) -> typing.Optional[utils.Field]: """Talk a user through creating a new field for their template""" # Here are some things we can use later message_check = lambda m: m.author == ctx.author and m.channel == ctx.channel okay_reaction_check = lambda r, u: str( r.emoji) in prompt_emoji and u.id == ctx.author.id prompt_emoji = [self.TICK_EMOJI, self.CROSS_EMOJI] messages_to_delete = [] # Ask if they want a new field if prompt_for_creation: field_message = await ctx.send( "Do you want to make a new field for your profile?", embed=template.build_embed()) messages_to_delete.append(field_message) for e in prompt_emoji: try: await field_message.add_reaction(e) except discord.Forbidden: try: await field_message.delete() except discord.NotFound: pass await ctx.send( "I tried to add a reaction to my message, but I was unable to. Please update my permissions for this channel and try again." ) return None # Here's us waiting for the "do you want to make a new field" reaction try: reaction, _ = await self.bot.wait_for( 'reaction_add', check=okay_reaction_check, timeout=120) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None # See if they don't wanna continue if str(reaction.emoji) == self.CROSS_EMOJI: return None await field_message.edit(content=field_message.content, embed=None) # Get a name for the new field v = await ctx.send( "What name should this field have? This is the name shown on the embed, so it should be something like 'Name', 'Age', 'Gender', etc." ) messages_to_delete.append(v) while True: try: field_name_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) messages_to_delete.append(field_name_message) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None # Check if if name is too long if 256 >= len(field_name_message.content) >= 1: break else: v = await ctx.send( "The maximum length of a field name is 256 characters. Please provide another name." ) messages_to_delete.append(v) field_name = field_name_message.content # Get a prompt for the field v = await ctx.send( "What message should I send when I'm asking people to fill out this field? This should be a question or prompt, eg 'What is your name/age/gender/etc'." ) messages_to_delete.append(v) while True: try: field_prompt_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) messages_to_delete.append(field_prompt_message) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None if len(field_prompt_message.content) >= 1: break else: v = await ctx.send( "You need to actually give text for the prompt :/") messages_to_delete.append(v) field_prompt = field_prompt_message.content prompt_is_command = bool( utils.CommandProcessor.COMMAND_REGEX.search(field_prompt)) # If it's a command, then we don't need to deal with this if not prompt_is_command: # Get field optional prompt_message = await ctx.send("Is this field optional?") messages_to_delete.append(prompt_message) for e in prompt_emoji: await prompt_message.add_reaction(e) try: field_optional_reaction, _ = await self.bot.wait_for( 'reaction_add', check=okay_reaction_check, timeout=120) field_optional_emoji = str(field_optional_reaction.emoji) except asyncio.TimeoutError: field_optional_emoji = self.CROSS_EMOJI field_optional = field_optional_emoji == self.TICK_EMOJI # Get timeout v = await ctx.send( "How many seconds should I wait for people to fill out this field (I recommend 120 - that's 2 minutes)? The minimum is 30, and the maximum is 600." ) messages_to_delete.append(v) while True: try: field_timeout_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) messages_to_delete.append(field_timeout_message) except asyncio.TimeoutError: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) return None try: timeout = int(field_timeout_message.content) if timeout < 30: raise ValueError() break except ValueError: v = await ctx.send( "I couldn't convert your message into a valid number - the minimum is 30 seconds. Please try again." ) messages_to_delete.append(v) field_timeout = min([timeout, 600]) # Ask for field type if image_set: text = f"What type is this field? Will you be getting numbers ({self.NUMBERS_EMOJI}), or any text ({self.LETTERS_EMOJI})?" else: text = f"What type is this field? Will you be getting numbers ({self.NUMBERS_EMOJI}), any text ({self.LETTERS_EMOJI}), or an image ({self.PICTURE_EMOJI})?" field_type_message = await ctx.send(text) messages_to_delete.append(field_type_message) # Add reactions await field_type_message.add_reaction(self.NUMBERS_EMOJI) await field_type_message.add_reaction(self.LETTERS_EMOJI) if not image_set: await field_type_message.add_reaction(self.PICTURE_EMOJI) # See what they said field_type_emoji = [ self.NUMBERS_EMOJI, self.LETTERS_EMOJI, self.PICTURE_EMOJI ] # self.TICK_EMOJI field_type_check = lambda r, u: str( r.emoji) in field_type_emoji and u == ctx.author try: reaction, _ = await self.bot.wait_for('reaction_add', check=field_type_check, timeout=120) emoji = str(reaction.emoji) except asyncio.TimeoutError: try: await ctx.send( "Picking a field type has timed out - defaulting to text." ) except (discord.Forbidden, discord.NotFound): pass emoji = self.LETTERS_EMOJI # Change that emoji into a datatype field_type = { self.NUMBERS_EMOJI: utils.NumberField, self.LETTERS_EMOJI: utils.TextField, self.PICTURE_EMOJI: utils.ImageField, }[emoji] if isinstance(field_type, utils.ImageField) and image_set: raise Exception( "You shouldn't be able to set two image fields.") # Set some defaults for the field stuff else: field_optional = False field_timeout = 15 field_type = utils.TextField # Make the field object field = utils.Field( field_id=uuid.uuid4(), name=field_name, index=index, prompt=field_prompt, timeout=field_timeout, field_type=field_type, template_id=template.template_id, optional=field_optional, deleted=False, ) # See if we need to delete things if delete_messages: await ctx.channel.purge( check=lambda m: m.id in [i.id for i in messages_to_delete], bulk=ctx.channel.permissions_for(ctx.guild.me).manage_messages) # And we done return field
async def create_new_field(self, ctx: utils.Context, profile_id: uuid.UUID, index: int, image_set: bool = False) -> utils.Field: """Lets a user create a new field in their profile""" # Ask if they want a new field field_message = await ctx.send( "Do you want to make a new field for your profile?") try: await field_message.add_reaction(self.TICK_EMOJI) await field_message.add_reaction(self.CROSS_EMOJI) except discord.Forbidden: try: await field_message.delete() except discord.NotFound: pass await ctx.send( "I tried to add a reaction to my message, but I was unable to. Please update my permissions for this channel and try again." ) return None # Here are some checks we can use later message_check = lambda m: m.author == ctx.author and m.channel == ctx.channel okay_reaction_check = lambda r, u: str(r.emoji) in [ self.TICK_EMOJI, self.CROSS_EMOJI ] and u.id == ctx.author.id # Here's us waiting for the "do you want to make a new field" reaction try: reaction, _ = await self.bot.wait_for('reaction_add', check=okay_reaction_check, timeout=120) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None # See if they don't wanna continue if str(reaction.emoji) == self.CROSS_EMOJI: return None # Get a name for the new field await ctx.send( "What name should this field have (eg: `Name`, `Age`, `Image`, etc - this is what shows on the embed)?" ) while True: try: field_name_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None # Check if if name is too long if 256 >= len(field_name_message.content) >= 1: break else: await ctx.send( "The maximum length of a field name is 256 characters. Please provide another name." ) field_name = field_name_message.content # Get a prompt for the field await ctx.send( f"What message should I send when I'm asking people to fill out this field (eg: `What is your {field_name.lower()}?`)?" ) while True: try: field_prompt_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None if len(field_prompt_message.content) >= 1: break else: await ctx.send( "You need to actually give text for the prompt :/") field_prompt = field_prompt_message.content # Get field optional prompt_message = await ctx.send(f"Is this field optional?") await prompt_message.add_reaction(self.TICK_EMOJI) await prompt_message.add_reaction(self.CROSS_EMOJI) try: field_optional_reaction, _ = await self.bot.wait_for( 'reaction_add', check=okay_reaction_check, timeout=120) field_optional_emoji = str(field_optional_reaction.emoji) except asyncio.TimeoutError: field_optional_emoji = self.CROSS_EMOJI field_optional = field_optional_emoji == self.TICK_EMOJI # Get timeout await ctx.send( "How many seconds should I wait for people to fill out this field (I recommend 120 - that's 2 minutes)? The minimum is 30, and the maximum is 600." ) while True: try: field_timeout_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) except asyncio.TimeoutError: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) return None try: timeout = int(field_timeout_message.content) if timeout < 30: raise ValueError() break except ValueError: await ctx.send( "I couldn't convert your message into a number - the minimum is 30 seconds. Please try again." ) field_timeout = min([timeout, 600]) # Ask for field type if image_set: text = f"What TYPE is this field? Will you be getting numbers ({self.NUMBERS_EMOJI}), or text ({self.LETTERS_EMOJI})?" else: text = f"What TYPE is this field? Will you be getting numbers ({self.NUMBERS_EMOJI}), text ({self.LETTERS_EMOJI}), or an image ({self.PICTURE_EMOJI})?" field_type_message = await ctx.send(text) # Add reactions await field_type_message.add_reaction(self.NUMBERS_EMOJI) await field_type_message.add_reaction(self.LETTERS_EMOJI) if not image_set: await field_type_message.add_reaction(self.PICTURE_EMOJI) # See what they said valid_emoji = [ self.NUMBERS_EMOJI, self.LETTERS_EMOJI, self.PICTURE_EMOJI ] # self.TICK_EMOJI field_type_check = lambda r, u: str( r.emoji) in valid_emoji and u == ctx.author try: reaction, _ = await self.bot.wait_for('reaction_add', check=field_type_check, timeout=120) emoji = str(reaction.emoji) except asyncio.TimeoutError: try: await ctx.send( "Picking a field type has timed out - defaulting to text.") except (discord.Forbidden, discord.NotFound): pass emoji = self.LETTERS_EMOJI # Change that emoji into a datatype field_type = { self.NUMBERS_EMOJI: utils.NumberField, self.LETTERS_EMOJI: utils.TextField, # self.TICK_EMOJI: utils.BooleanField, # TODO self.PICTURE_EMOJI: utils.ImageField, }.get(emoji, Exception("Shouldn't be reached."))() if isinstance(field_type, utils.ImageField) and image_set: raise Exception("You shouldn't be able to set two image fields.") # Make the field object return utils.Field( field_id=uuid.uuid4(), name=field_name, index=index, prompt=field_prompt, timeout=field_timeout, field_type=field_type, profile_id=profile_id, optional=field_optional, deleted=False, )
async def create_new_field( self, ctx: utils.Context, template: localutils.Template, index: int, image_set: bool = False, prompt_for_creation: bool = True, delete_messages: bool = False ) -> typing.Optional[localutils.Field]: """ Talk a user through creating a new field for their template. """ # Here are some things we can use later def message_check(message): return all([ message.author.id == ctx.author.id, message.channel.id == ctx.channel.id, ]) messages_to_delete = [] # Ask if they want a new field if prompt_for_creation: field_message = await ctx.send( "Do you want to make a new field for your profile?", embed=template.build_embed(self.bot), components=utils.MessageComponents.boolean_buttons(), ) messages_to_delete.append(field_message) # Here's us waiting for the "do you want to make a new field" reaction try: payload = await field_message.wait_for_button_click( check=lambda p: p.user.id == ctx.author.id, timeout=120) await payload.ack() except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) except (discord.Forbidden, discord.NotFound): pass return None # See if they don't wanna continue if payload.component.custom_id == "NO": return None await field_message.edit(content=field_message.content, embed=None) # Get a name for the new field field_name = await self.get_field_name(ctx, messages_to_delete) if field_name is None: return # Get a prompt for the field v = await ctx.send(( "What message should I send when I'm asking people to fill out this field? " "This should be a question or prompt, eg 'What is your name/age/gender/etc'." )) messages_to_delete.append(v) while True: try: field_prompt_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) messages_to_delete.append(field_prompt_message) except asyncio.TimeoutError: try: await ctx.send( "Creating a new field has timed out. The profile is being " "created with the fields currently added.") except (discord.Forbidden, discord.NotFound): pass return None if len(field_prompt_message.content) >= 1: break else: v = await ctx.send( "You need to actually give text for the prompt :/") messages_to_delete.append(v) field_prompt = field_prompt_message.content prompt_is_command = bool( localutils.CommandProcessor.COMMAND_REGEX.search(field_prompt)) # If it's a command, then we don't need to deal with this if not prompt_is_command: # Get field optional prompt_message = await ctx.send( "Is this field optional?", components=utils.MessageComponents.boolean_buttons(), ) messages_to_delete.append(prompt_message) try: payload = await prompt_message.wait_for_button_click( check=lambda p: p.user.id == ctx.author.id, timeout=120) await payload.ack() field_optional_emoji = payload.component.custom_id except asyncio.TimeoutError: field_optional_emoji = "NO" field_optional = field_optional_emoji == "YES" self.bot.loop.create_task( payload.message.edit(components=utils.MessageComponents. boolean_buttons().disable_components(), )) # Get timeout v = await ctx.send(( "How many seconds should I wait for people to fill out this field (I recommend 120 - " "that's 2 minutes)? The minimum is 30, and the maximum is 600." )) messages_to_delete.append(v) while True: try: field_timeout_message = await self.bot.wait_for( 'message', check=message_check, timeout=120) messages_to_delete.append(field_timeout_message) except asyncio.TimeoutError: await ctx.send( "Creating a new field has timed out. The profile is being created with the fields currently added." ) return None try: timeout = int(field_timeout_message.content) if timeout < 30: raise ValueError() break except ValueError: v = await ctx.send( "I couldn't convert your message into a valid number - the minimum is 30 seconds. Please try again." ) messages_to_delete.append(v) field_timeout = min([timeout, 600]) # Ask for a field type action_row = utils.ActionRow() action_row.add_component(utils.Button("Text", custom_id="TEXT")) action_row.add_component( utils.Button("Numbers", custom_id="NUMBERS")) if not image_set: action_row.add_component( utils.Button("Image", custom_id="IMAGE")) components = utils.MessageComponents(action_row) field_type_message = await ctx.send("What type is this field?", components=components) messages_to_delete.append(field_type_message) # See what they said try: payload = await field_type_message.wait_for_button_click( check=lambda p: p.user.id == ctx.author.id, timeout=120) await payload.ack() key = payload.component.custom_id except asyncio.TimeoutError: try: await ctx.send( "Picking a field type has timed out - defaulting to text." ) except (discord.Forbidden, discord.NotFound): pass key = "TEXT" # Change that emoji into a datatype field_type = { "NUMBERS": localutils.NumberField, "TEXT": localutils.TextField, "IMAGE": localutils.ImageField, }[key] if isinstance(field_type, localutils.ImageField) and image_set: raise Exception( "You shouldn't be able to set two image fields.") # Set some defaults for the field stuff else: field_optional = False field_timeout = 15 field_type = localutils.TextField # Make the field object field = localutils.Field( field_id=uuid.uuid4(), name=field_name, index=index, prompt=field_prompt, timeout=field_timeout, field_type=field_type, template_id=template.template_id, optional=field_optional, deleted=False, ) # See if we need to delete things if delete_messages: self.purge_message_list(ctx.channel, messages_to_delete) # And we done return field
async def update_template_field(request: Request): """ The route that handles the updating of the template metadata. """ # Make sure the user is logged in session = await aiohttp_session.get_session(request) if not session.get("user_id"): return json_response({"error": "Not logged in."}, status=401) # Try and read the POST data from the user required_fields = {"template_id", "name", "prompt", "timeout", "type", "optional"} field_converters = {"timeout": int, "optional": bool} try: data = await request.json() data = {i: o.strip() or None for i, o in data.items()} for f in required_fields: data.setdefault(f, None) if f in field_converters: data[f] = field_converters[f](data[f]) except ValueError: return json_response({"error": "Couldn't validate the given POST data."}, status=400) except Exception: return json_response({"error": "Could not read POST data."}, status=400) # Now we want to check things with the database async with request.app['database']() as db: # Make sure the template exists field = None if data.get('field_id'): field_rows = await db("""SELECT * FROM field WHERE field_id=$1""", data['field_id']) if not field_rows: return json_response({"error": "Field does not exist."}, status=400) field = localutils.Field(**field_rows[0]) template = await localutils.Template.fetch_template_by_id(db, field.template_id if field else data['template_id'], fetch_fields=False) if not template: return json_response({"error": "Template does not exist."}, status=400) # Make sure the user is editing a template that they have permission to edit try: guild = await request.app['bots']['bot'].fetch_guild(template.guild_id) except discord.HTTPException: return json_response({"error": "Bot not in guild."}, status=401) try: member = await guild.fetch_member(session.get("user_id")) except discord.HTTPException: return json_response({"error": "User not in guild."}, status=401) if not member.guild_permissions.manage_guild: return json_response({"error": "Member cannot manage guild."}, status=401) # Update the template if field: updated_rows = await db( """UPDATE field SET name=$2, prompt=$3, timeout=$4, field_type=$5, optional=$6 WHERE field_id=$1 RETURNING *""", data['field_id'], data['name'], data['prompt'], data['timeout'], data['type'], data['optional'], ) else: updated_rows = await db( """INSERT INTO field (field_id, name, prompt, timeout, field_type, optional, template_id, index) VALUES (GEN_RANDOM_UUID(), $1, $2, $3, $4, $5, $6, COALESCE((SELECT MAX(index) FROM field WHERE template_id=$6) + 1, 0)) RETURNING *""", data['name'], data['prompt'], data['timeout'], data['type'], data['optional'], template.template_id, ) # Return ret_data = dict(updated_rows[0]) ret_data['field_id'] = str(ret_data['field_id']) ret_data['template_id'] = str(ret_data['template_id']) return json_response({"error": "", "data": ret_data}, status=200)