async def on_command_error(self, ctx: utils.Context, error: commands.CommandError): """ Listens for command not found errors and tries to run them as interactions. """ if not isinstance(error, commands.CommandNotFound): return # Deal with common aliases command_name = ctx.invoked_with.lower() for aliases in COMMON_COMMAND_ALIASES: if command_name in aliases: command_name = aliases[0] # See if we wanna deal with it guild_ids = [0] if ctx.guild is None else [0, ctx.guild.id] async with self.bot.database() as db: rows = await db( "SELECT response FROM interaction_text WHERE interaction_name=$1 AND guild_id=ANY($2::BIGINT[]) ORDER BY RANDOM() LIMIT 1", command_name.lower(), guild_ids) if not rows: self.logger.info("Nothing found") return # No responses found # Create a command we can invoke ctx.interaction_response = rows[0]['response'] ctx.interaction_name = command_name ctx.invoke_meta = True ctx.command = self.bot.get_command("interaction_command_meta") await self.bot.invoke(ctx)
async def displayplant(self, ctx: utils.Context, user: typing.Optional[utils.converters.UserID], *, plant_name: str): """ Shows you your plant status. """ # Get data from database user = discord.Object(user) if user else ctx.author async with self.bot.database() as db: plant_rows = await db( "SELECT * FROM plant_levels WHERE user_id=$1 AND LOWER(plant_name)=LOWER($2)", user.id, plant_name) if not plant_rows: return await ctx.send( f"You have no plant named **{plant_name.capitalize()}**", allowed_mentions=discord.AllowedMentions(users=False, roles=False, everyone=False)) # Filter into variables display_utils = self.bot.get_cog("PlantDisplayUtils") if plant_rows: display_data = display_utils.get_display_data(plant_rows[0], user_id=user.id) else: display_data = display_utils.get_display_data(None, user_id=user.id) # Generate text if display_data['plant_type'] is None: if ctx.author.id == user.id: text = f"You don't have a plant yet, {ctx.author.mention}! Run `{ctx.prefix}getplant` to get one!" else: text = f"<@{user.id}> doesn't have a plant yet!" else: text = f"<@{user.id}>'s {display_data['plant_type'].replace('_', ' ')} - **{plant_rows[0]['plant_name']}**" if int(display_data['plant_nourishment']) > 0: if ctx.author.id == user.id: text += "!" elif int(display_data['plant_nourishment']) < 0: if ctx.author.id == user.id: text += f". It's looking a tad... dead. Run `{ctx.prefix}deleteplant {plant_name}` to plant some new seeds." else: text += ". It looks a bit... worse for wear, to say the least." elif int(display_data['plant_nourishment']) == 0: if ctx.author.id == user.id: text += f". There are some seeds there, but you need to `{ctx.prefix}water {plant_rows[0]['plant_name']}` them to get them to grow." else: text += ". There are some seeds there I think, but they need to be watered." # Send image image_data = display_utils.image_to_bytes( display_utils.get_plant_image(**display_data)) file = discord.File(image_data, filename="plant.png") embed = utils.Embed( use_random_colour=True, description=text).set_image("attachment://plant.png") ctx._set_footer(embed) await ctx.send(embed=embed, file=file)
async def plants(self, ctx: utils.Context, user: typing.Optional[discord.User]): """ Shows you all the plants that a given user has. """ # Grab the plant data user = user or ctx.author async with self.bot.database() as db: user_rows = await db( "SELECT * FROM plant_levels WHERE user_id=$1 ORDER BY plant_name DESC", user.id) # See if they have anything available plant_data = sorted([ (i['plant_name'], i['plant_type'], i['plant_nourishment'], i['last_water_time'], i['plant_adoption_time']) for i in user_rows ]) if not plant_data: embed = utils.Embed(use_random_colour=True, description=f"<@{user.id}> has no plants :c") return await ctx.send(embed=embed) # Add the plant information embed = utils.Embed(use_random_colour=True, description=f"<@{user.id}>'s plants") ctx._set_footer(embed) for plant_name, plant_type, plant_nourishment, last_water_time, plant_adoption_time in plant_data: plant_type_display = plant_type.replace('_', ' ').capitalize() plant_death_time = last_water_time + timedelta( **self.bot.config.get('plants', {}).get( 'death_timeout', {'days': 3})) plant_death_humanize_time = utils.TimeValue( (plant_death_time - dt.utcnow()).total_seconds()).clean_full plant_life_humanize_time = utils.TimeValue( (dt.utcnow() - plant_adoption_time).total_seconds()).clean_full if plant_nourishment == 0: text = f"{plant_type_display}, nourishment level {plant_nourishment}/{self.bot.plants[plant_type].max_nourishment_level}." elif plant_nourishment > 0: text = ( f"**{plant_type_display}**, nourishment level {plant_nourishment}/{self.bot.plants[plant_type].max_nourishment_level}.\n" f"If not watered, this plant will die in **{plant_death_humanize_time}**.\n" f"This plant has been alive for **{plant_life_humanize_time}**.\n" ) else: text = f"{plant_type_display}, dead :c" embed.add_field(plant_name, text, inline=False) # Return to user v = await ctx.send(embed=embed) try: await self.bot.add_delete_button(v, ( ctx.author, user, ), wait=False) except discord.HTTPException: pass
async def inventory(self, ctx: utils.Context, user: utils.converters.UserID = None): """ Show you the inventory of a user. """ # Get user info user = discord.Object(user) if user else ctx.author async with self.bot.database() as db: user_rows = await db( "SELECT * FROM user_settings WHERE user_id=$1", user.id) plant_rows = await db( "SELECT * FROM plant_levels WHERE user_id=$1", user.id) user_inventory_rows = await db( "SELECT * FROM user_inventory WHERE user_id=$1 AND amount > 0", user.id) # Start our embed embed = utils.Embed(use_random_colour=True, description="") ctx._set_footer(embed) # Format exp into a string if user_rows: exp_value = user_rows[0]['user_experience'] else: exp_value = 0 embed.description += f"<@{user.id}> has **{exp_value:,}** experience.\n" # Format plant limit into a string if user_rows: plant_limit = user_rows[0]['plant_limit'] else: plant_limit = 1 they_you = {True: "you", False: "they"}.get(user.id == ctx.author.id) their_your = { True: "your", False: "their" }.get(user.id == ctx.author.id) if plant_limit == len(plant_rows): embed.description += f"{they_you.capitalize()} are currently using all of {their_your} available {plant_limit} plant pots.\n" else: embed.description += f"{they_you.capitalize()} are currently using {len(plant_rows)} of {their_your} available {plant_limit} plant pots.\n" # Format inventory into a string if user_inventory_rows: inventory_string = "\n".join([ f"{row['item_name'].replace('_', ' ').capitalize()} x{row['amount']:,}" for row in user_inventory_rows ]) embed.add_field("Inventory", inventory_string) # Return to user return await ctx.send(embed=embed)
async def on_command_error(self, ctx: utils.Context, error: commands.CommandError): """ CommandNotFound handler so the bot can search for that custom command. """ # Filter out DMs if isinstance(ctx.channel, discord.DMChannel): return # Fail silently on DM invocation # Handle commandnotfound which is really just handling the set/get/delete/etc commands if not isinstance(error, commands.CommandNotFound): return # Get the command and used template prefixless_content = ctx.message.content[len(ctx.prefix):] matches = self.COMMAND_REGEX.search(prefixless_content) if matches is None: matches = self.OLD_COMMAND_REGEX.search(prefixless_content) if matches is None: return command_operator = matches.group("command") # get/get/delete/edit template_name = matches.group("template") # template name # Find the template they asked for on their server async with self.bot.database() as db: template = await localutils.Template.fetch_template_by_name( db, ctx.guild.id, template_name, fetch_fields=False) if not template: self.logger.info( f"Failed at getting template '{template_name}' in guild {ctx.guild.id}" ) return # Fail silently on template doesn't exist # Invoke command metacommand: utils.Command = self.bot.get_command( f'{command_operator.lower()}_profile_meta') ctx.command = metacommand ctx.template = template ctx.invoke_meta = True ctx.invoked_with = f"{matches.group('template')} {matches.group('command')}" ctx.view = commands.view.StringView(matches.group('args')) try: self.bot.dispatch("command", ctx) await metacommand.invoke( ctx) # This converts the args for me, which is nice except (commands.CommandInvokeError, commands.CommandError) as e: self.bot.dispatch( "command_error", ctx, e ) # Throw any errors we get in this command into its own error handler
async def plants(self, ctx: utils.Context, user: utils.converters.UserID = None): """ Shows you all the plants that a given user has. """ # Grab the plant data user = discord.Object(user) if user else ctx.author async with self.bot.database() as db: user_rows = await db( "SELECT * FROM plant_levels WHERE user_id=$1 ORDER BY plant_name DESC", user.id) # See if they have anything available plant_data = sorted([(i['plant_name'], i['plant_type'], i['plant_nourishment'], i['last_water_time']) for i in user_rows]) if not plant_data: embed = utils.Embed(use_random_colour=True, description=f"<@{user.id}> has no plants :c") return await ctx.send(embed=embed) # Add the plant information embed = utils.Embed(use_random_colour=True, description=f"<@{user.id}>'s plants") ctx._set_footer(embed) for plant_name, plant_type, plant_nourishment, last_water_time in plant_data: plant_type_display = plant_type.replace('_', ' ').capitalize() # plant_name_display = re.sub(r"([\_*`])", r"\\\1", plant_name) plant_death_time = last_water_time + timedelta( **self.bot.config.get('plants', {}).get( 'death_timeout', {'days': 3})) plant_death_humanize_time = arrow.get(plant_death_time).humanize( granularity=["day", "hour", "minute"], only_distance=True) if plant_nourishment == 0: text = f"{plant_type_display}, nourishment level {plant_nourishment}/{self.bot.plants[plant_type].max_nourishment_level}." elif plant_nourishment > 0: text = ( f"{plant_type_display}, nourishment level {plant_nourishment}/{self.bot.plants[plant_type].max_nourishment_level}.\n" f"If not watered, this plant will die in *{plant_death_humanize_time}*." ) else: text = f"{plant_type_display}, dead :c" embed.add_field(plant_name, text) # Return to user return await ctx.send(embed=embed)
async def dnd_class(self, ctx: vbu.Context, *, class_name: str): """ Gives you information on a D&D class. """ async with ctx.typing(): data = await self.send_web_request("classes", class_name) if not data: return await ctx.send( "I couldn't find any information for that class.", wait=False) embed = vbu.Embed( use_random_colour=True, title=data['name'], ).add_field( "Proficiencies", ", ".join([i['name'] for i in data['proficiencies']]), ).add_field( "Saving Throws", ", ".join([i['name'] for i in data['saving_throws']]), ).add_field( "Starting Equipment", "\n".join([ f"{i['quantity']}x {i['equipment']['name']}" for i in data['starting_equipment'] ]), ) return await ctx.send(embed=embed, wait=False)
async def runstartupmethod(self, ctx: vbu.Context): """ Runs the bot startup method, recaching everything of interest. """ async with ctx.typing(): await self.bot.startup() await ctx.okay()
async def waterplant(self, ctx: utils.Context, *, plant_name: str): """ Increase the growth level of your plant. """ # Let's run all the bullshit item = await self.water_plant_backend(ctx.author.id, plant_name) if item['success'] is False: return await ctx.send(item['text']) output_lines = item['text'].split("\n") # Try and embed the message embed = None if ctx.guild is None or ctx.channel.permissions_for( ctx.guild.me).embed_links: # Make initial embed embed = utils.Embed(use_random_colour=True, description=output_lines[0]) # Add multipliers if len(output_lines) > 1: embed.add_field("Multipliers", "\n".join( [i.strip('') for i in output_lines[1:]]), inline=False) # Add "please vote for Flower" footer counter = 0 ctx._set_footer(embed) def check(footer_text) -> bool: if item['voted_on_topgg']: return 'vote' not in footer_text return 'vote' in footer_text while counter < 100 and check(embed.footer.text.lower()): ctx._set_footer(embed) counter += 1 # Clear the text we would otherwise output output_lines.clear() # Send message return await ctx.send("\n".join(output_lines), embed=embed)
async def leaderboard(self, ctx: utils.Context, pages: int = 1): """ Gives you the leaderboard users for the server. """ # This takes a while async with ctx.typing(): # Get all their valid user IDs async with self.bot.database() as db: message_rows = await db( """SELECT user_id, COUNT(timestamp) FROM user_messages WHERE guild_id=$1 AND timestamp > TIMEZONE('UTC', NOW()) - INTERVAL '7 days' GROUP BY user_id ORDER BY COUNT(timestamp) DESC LIMIT 30;""", ctx.guild.id, ) vc_rows = await db( """SELECT user_id, COUNT(timestamp) FROM user_vc_activity WHERE guild_id=$1 AND timestamp > TIMEZONE('UTC', NOW()) - INTERVAL '7 days' GROUP BY user_id ORDER BY COUNT(timestamp) DESC LIMIT 30;""", ctx.guild.id, ) # Sort that into more formattable data user_data_dict = collections.defaultdict({ 'message_count': 0, 'vc_minute_count': 0 }.copy) # uid: {message_count: int, vc_minute_count: int} for row in message_rows: user_data_dict[row['user_id']]['message_count'] = row['count'] for row in vc_rows: user_data_dict[ row['user_id']]['vc_minute_count'] = row['count'] # And now make it into something we can sort guild_user_data = [(uid, d['message_count'], d['vc_minute_count']) for uid, d in user_data_dict.items()] valid_guild_user_data = [] for i in guild_user_data: try: if ctx.guild.get_member( i[0]) or await ctx.guild.fetch_member(i[0]): valid_guild_user_data.append(i) except discord.HTTPException: pass ordered_guild_user_data = sorted(valid_guild_user_data, key=lambda k: k[1] + (k[2] // 5), reverse=True) # Make menu pages = menus.MenuPages(source=LeaderboardSource( self.bot, ordered_guild_user_data, "Tracked Points over 7 days"), clear_reactions_after=True) return await pages.start(ctx)
async def displayall(self, ctx: utils.Context, user: typing.Optional[discord.User]): """ Show you all of your plants. """ # Get data from database user = user or ctx.author async with self.bot.database() as db: plant_rows = await db( "SELECT * FROM plant_levels WHERE user_id=$1 ORDER BY plant_name DESC", user.id) if not plant_rows: return await ctx.send(f"<@{user.id}> has no available plants.", allowed_mentions=discord.AllowedMentions( users=[ctx.author])) await ctx.trigger_typing() # Filter into variables display_utils = self.bot.get_cog("PlantDisplayUtils") plant_rows = display_utils.sort_plant_rows(plant_rows) images = [] for plant_row in plant_rows: if plant_row: display_data = display_utils.get_display_data(plant_row, user_id=user.id) else: display_data = display_utils.get_display_data(None, user_id=user.id) images.append(display_utils.get_plant_image(**display_data)) # Get our images image = display_utils.compile_plant_images(*images) image_to_send = display_utils.image_to_bytes(image) text = f"Here are all of <@{user.id}>'s plants!" file = discord.File(image_to_send, filename="plant.png") embed = utils.Embed( use_random_colour=True, description=text).set_image("attachment://plant.png") ctx._set_footer(embed) await ctx.send(embed=embed, file=file)
async def volunteer(self, ctx: utils.Context): """ Get the information for volunteering. """ VOLUNTEER_INFORMATION = ( "Want to help out with Flower, and watch it grow? Heck yeah! There's a few ways you can help out:\n\n" "**Art**\n" "Flower takes a lot of art, being a bot entirely about watching things grow. Unfortunately, I'm awful at art. Anything you can help out " "with would be amazing, if you had some kind of artistic talent yourself. If you [go here](https://github.com/Voxel-Fox-Ltd/Flower/blob/master/images/pots/clay/full.png) " "you can get an empty pot image you can use as a base. Every plant in Flower has a minimum of 6 distinct growth stages " "(which you can [see here](https://github.com/Voxel-Fox-Ltd/Flower/tree/master/images/plants/blue_daisy/alive) if you need an example).\n" "If this is the kind of thing you're interested in, I suggest you join [the support server](https://discord.gg/vfl) to ask for more information, or " "[email Kae](mailto://[email protected]) - the bot's developer.\n" "\n" "**Programming**\n" "If you're any good at programming, you can help out on [the bot's Github](https://github.com/Voxel-Fox-Ltd/Flower)! Ideas are discussed on " "[the support server](https://discord.gg/vfl) if you want to do that, but otherwise you can PR fixes, add issues, etc from there as you would " "with any other git repository.\n" "\n" "**Ideas**\n" "Flower is in constant need of feedback from the people who like to use it, and that's where you can shine. Even if you don't want to " "help out with art, programming, or anything else: it would be _absolutely amazing_ if you could give your experiences, gripes, and suggestions for " "Flower via the `{ctx.clean_prefix}suggest` command. That way I know where to change things, what to do to add new stuff, etcetc. If you want to " "discuss in more detail, I'm always around on [the support server](https://discord.gg/vfl)." ).format(ctx=ctx) embed = utils.Embed( use_random_colour=True, description=VOLUNTEER_INFORMATION, ) ctx._set_footer(embed) try: await ctx.author.send(embed=embed) except discord.HTTPException: return await ctx.send("I wasn't able to send you a DM :<") if ctx.guild is not None: return await ctx.send("Sent you a DM!")
async def dnd_condition(self, ctx:utils.Context, *, condition_name:str): """ Gives you information on a D&D condition. """ async with ctx.typing(): data = await self.send_web_request("conditions", condition_name) if not data: return await ctx.send("I couldn't find any information for that condition.") embed = utils.Embed( use_random_colour=True, title=data['name'], description="\n".join(data['desc']), ) return await ctx.send(embed=embed)
async def relationship(self, ctx: vbu.Context, user: vbu.converters.UserID, other: vbu.converters.UserID = None): """ Gets the relationship between the two specified users. """ # Fix up the arguments if other is None: user_id, other_id = ctx.author.id, user else: user_id, other_id = user, other # See if they're the same person if user_id == other_id: if user_id == ctx.author.id: return await ctx.send( "Unsurprisingly, you're pretty closely related to yourself.", wait=False) return await ctx.send( "Unsurprisingly, they're pretty closely related to themselves.", wait=False) # Get their relation user_info, other_info = utils.FamilyTreeMember.get_multiple( user_id, other_id, guild_id=utils.get_family_guild_id(ctx)) async with ctx.typing(): relation = user_info.get_relation(other_info) # Get names user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) other_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, other_id) # Output if relation is None: output = f"**{utils.escape_markdown(user_name)}** is not related to **{utils.escape_markdown(other_name)}**." if user_id == ctx.author.id: output = f"You're not related to **{utils.escape_markdown(other_name)}**." else: output = f"**{utils.escape_markdown(other_name)}** is **{utils.escape_markdown(user_name)}**'s {relation}." if user_id == ctx.author.id: output = f"**{utils.escape_markdown(other_name)}** is your {relation}." return await ctx.send(output, allowed_mentions=discord.AllowedMentions.none(), wait=False)
async def issue_comment(self, ctx:utils.Context, repo:GitRepo, issue:GitIssueNumber, *, comment:str): """ Comment on a git issue. """ # Get the database because whatever why not async with self.bot.database() as db: user_rows = await db("SELECT * FROM user_settings WHERE user_id=$1", ctx.author.id) if not user_rows or not user_rows[0][f'{repo.host.lower()}_username']: return await ctx.send(f"You need to link your {repo.host} account to Discord to run this command - see `{ctx.clean_prefix}website`.") # Add attachments attachment_urls = [] for i in ctx.message.attachments: async with ctx.typing(): try: async with self.bot.session.get(i.url) as r: data = await r.read() file = discord.File(io.BytesIO(data), filename=i.filename) cache_message = await ctx.author.send(file=file) attachment_urls.append((file.filename, cache_message.attachments[0].url)) except discord.HTTPException: break # Get the headers if repo.host == "Github": headers = {'Accept': 'application/vnd.github.v3+json','Authorization': f"token {user_rows[0]['github_access_token']}",} elif repo.host == "Gitlab": headers = {'Authorization': f"Bearer {user_rows[0]['gitlab_bearer_token']}"} json = {'body': (comment + "\n\n" + "\n".join([f"![{name}]({url})" for name, url in attachment_urls])).strip()} headers.update({'User-Agent': self.bot.user_agent}) # Create comment async with self.bot.session.post(repo.issue_comments_api_url.format(issue=issue), json=json, headers=headers) as r: data = await r.json() self.logger.info(f"Received data from git {r.url!s} - {data!s}") if r.status == 404: return await ctx.send("I was unable to find that issue.") if 200 <= r.status < 300: pass else: return await ctx.send(f"I was unable to create a comment on that issue - `{data}`.") # Output if repo.host == "Github": return await ctx.send(f"Comment added! <{data['html_url']}>") return await ctx.send(f"Comment added! <https://gitlab.com/{repo.owner}/{repo.repo}/-/issues/{issue}#note_{data['id']}>")
async def copulate(self, ctx: vbu.Context, target: discord.Member): """ Lets you... um... heck someone. """ # Variables we're gonna need for later family_guild_id = utils.get_family_guild_id(ctx) author_tree, target_tree = utils.FamilyTreeMember.get_multiple( ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not a bot if target.id == self.bot.user.id: return await ctx.send("Ew. No. Thanks.", wait=False) if target.id == ctx.author.id: return # See if they're already related async with ctx.typing(): relation = author_tree.get_relation(target_tree) if relation and relation != "partner" and utils.guild_allows_incest( ctx) is False: return await ctx.send( f"Woah woah woah, it looks like you guys are related! {target.mention} is your {relation}!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Set up the proposal if target.id != ctx.author.id: try: result = await utils.send_proposal_message( ctx, target, f"Hey, {target.mention}, {ctx.author.mention} do you wanna... smash? \N{SMIRKING FACE}", allow_bots=True, ) except Exception: result = None if result is None: return # Respond await result.ctx.send( random.choice(utils.random_text.Copulate.VALID).format( author=ctx.author, target=target), wait=False, )
async def dnd_spell(self, ctx:utils.Context, *, spell_name:str): """ Gives you information on a D&D spell. """ async with ctx.typing(): data = await self.send_web_request("spells", spell_name) if not data: return await ctx.send("I couldn't find any information for that spell.") embed = utils.Embed( use_random_colour=True, title=data['name'], description=data['desc'][0], ).add_field( "Casting Time", data['casting_time'], ).add_field( "Range", data['range'], ).add_field( "Components", ', '.join(data['components']), ).add_field( "Material", data.get('material', 'N/A'), ).add_field( "Duration", data['duration'], ).add_field( "Classes", ', '.join([i['name'] for i in data['classes']]), ).add_field( "Ritual", data['ritual'], ).add_field( "Concentration", data['concentration'], ) if data.get('higher_level'): embed.add_field( "Higher Level", "\n".join(data['higher_level']), inline=False, ) elif data.get('damage'): text = "" if data['damage'].get('damage_at_character_level'): text += "\nCharacter level " + ", ".join([f"{i}: {o}" for i, o in data['damage']['damage_at_character_level'].items()]) if data['damage'].get('damage_at_slot_level'): text += "\nSlot level " + ", ".join([f"{i}: {o}" for i, o in data['damage']['damage_at_slot_level'].items()]) embed.add_field( "Damage", text.strip(), inline=False, ) return await ctx.send(embed=embed)
async def setupsupportguild(self, ctx:utils.Context): """ Sends some sexy new messages into the support guild. """ # Make sure we're in the right guild if ctx.guild is None or ctx.guild.id != SUPPORT_GUILD_ID: return await ctx.send("This can only be run on the set support guild.") # This could take a while async with ctx.typing(): # Remake the FAQ channel for each channel for channel_id_str, embed_lines in FAQ_MESSAGES.items(): # Get the category object channel = self.bot.get_channel(int(channel_id_str)) category = channel.category # Get the faq channel and delete the old message faq_channel = category.channels[0] if faq_channel.name != "faqs": return await ctx.send( f"The first channel in the **{category_name}** category isn't called **faqs**.", allowed_mentions=discord.AllowedMentions.none(), ) # Make the embed emoji_lines = [f"{index}\N{COMBINING ENCLOSING KEYCAP} **{string}**" for index, string in enumerate(embed_lines, start=1)] description = "\n".join(emoji_lines + ["\N{BLACK QUESTION MARK ORNAMENT} **Other**"]) new_embed = utils.Embed(title="What issue are you having?", description=description, colour=0x1) # See if it's anything new current_messages = await faq_channel.history(limit=1).flatten() if current_messages and current_messages[0].embeds and current_messages[0].embeds[0].to_dict() == new_embed.to_dict(): continue await current_messages[0].delete() new_message = await faq_channel.send(embed=new_embed) for emoji, item in [i.strip().split(" ", 1) for i in new_message.embeds[0].description.strip().split("\n")]: await new_message.add_reaction(emoji) # And we should be done at this point await ctx.okay()
async def dnd_monster(self, ctx:utils.Context, *, monster_name:str): """ Gives you information on a D&D monster. """ async with ctx.typing(): data = await self.send_web_request("monsters", monster_name) if not data: return await ctx.send("I couldn't find any information for that monster.") embed = utils.Embed( use_random_colour=True, title=data['name'], description="\n".join([ f"{data['size'].capitalize()} | {data['type']} | {data['hit_points']:,} ({data['hit_dice']}) HP | {data['xp']:,} XP", ", ".join([f"{o} {data[i]}" for i, o in self.ATTRIBUTES.items()]), ]) ).add_field( "Proficiencies", ", ".join([f"{i['proficiency']['name']} {i['value']}" for i in data['proficiencies']]) or "None", ).add_field( "Damage Vulnerabilities", "\n".join(data['damage_vulnerabilities']).capitalize() or "None", ).add_field( "Damage Resistances", "\n".join(data['damage_resistances']).capitalize() or "None", ).add_field( "Damage Immunities", "\n".join(data['damage_immunities']).capitalize() or "None", ).add_field( "Condition Immunities", "\n".join([i['name'] for i in data['condition_immunities']]).capitalize() or "None", ).add_field( "Senses", "\n".join([f"{i.replace('_', ' ').capitalize()} {o}" for i, o in data['senses'].items()]) or "None", ) self.group_field_descriptions(embed, "Actions", data['actions']) self.group_field_descriptions(embed, "Legendary Actions", data.get('legendary_actions', list())) if data.get('special_abilities'): embed.add_field( "Special Abilities", "\n".join([f"**{i['name']}**\n{i['desc']}" for i in data['special_abilities'] if i['name'] != 'Spellcasting']) or "None", inline=False, ) spellcasting = [i for i in data.get('special_abilities', list()) if i['name'] == 'Spellcasting'] if spellcasting: spellcasting = spellcasting[0] embed.add_field( "Spellcasting", spellcasting['desc'].replace('\n\n', '\n'), inline=False, ) return await ctx.send(embed=embed)
async def httpcat(self, ctx: vbu.Context, errorcode: str): """ Gives you a cat based on an HTTP error code. """ standard_errorcodes = [error.value for error in http.HTTPStatus] if errorcode in ('random', 'rand', 'r'): errorcode = random.choice(standard_errorcodes) else: try: errorcode = int(errorcode) except ValueError: return ctx.send( 'Converting to "int" failed for parameter "errorcode".', wait=False) await ctx.trigger_typing() headers = {"User-Agent": self.bot.user_agent} async with self.bot.session.get(f"https://http.cat/{errorcode}", headers=headers) as r: if r.status == 404: if errorcode not in standard_errorcodes: await ctx.send("That HTTP code doesn't exist.", wait=False) else: await ctx.send( 'Image for HTTP code not found on provider.', wait=False) return if r.status != 200: await ctx.send( f'Something went wrong, try again later. ({r.status})', wait=False) return with vbu.Embed(use_random_colour=True) as embed: embed.set_image(url=f'https://http.cat/{errorcode}') await ctx.send(embed=embed, wait=False)
async def makeparent(self, ctx: vbu.Context, *, target: utils.converters.UnblockedMember): """ Picks a user that you want to be your parent. """ # Variables we're gonna need for later family_guild_id = utils.get_family_guild_id(ctx) author_tree, target_tree = utils.FamilyTreeMember.get_multiple( ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not themselves if target.id == ctx.author.id: return await ctx.send( "That's you. You can't make yourself your parent.", wait=False) # Check they're not a bot if target.id == self.bot.user.id: return await ctx.send( "I think I could do better actually, but thank you!", wait=False) # Lock those users re = await self.bot.redis.get_connection() try: lock = await utils.ProposalLock.lock(re, ctx.author.id, target.id) except utils.ProposalInProgress: return await ctx.send( "Aren't you popular! One of you is already waiting on a proposal - please try again later.", wait=False) # See if the *target* is already married if author_tree.parent: await lock.unlock() return await ctx.send( f"Hey! {ctx.author.mention}, you already have a parent \N{ANGRY FACE}", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # See if we're already married if ctx.author.id in target_tree._children: await lock.unlock() return await ctx.send( f"Hey isn't {target.mention} already your child? \N{FACE WITH ROLLING EYES}", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # See if they're already related async with ctx.typing(): relation = author_tree.get_relation(target_tree) if relation and utils.guild_allows_incest(ctx) is False: await lock.unlock() return await ctx.send( f"Woah woah woah, it looks like you guys are already related! {target.mention} is your {relation}!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Manage children children_amount = await self.get_max_children_for_member( ctx.guild, target) if len(target_tree._children) >= children_amount: return await ctx.send( f"They're currently at the maximum amount of children they can have - see `{ctx.prefix}perks` for more information.", wait=False, ) # Check the size of their trees # TODO I can make this a util because I'm going to use it a couple times max_family_members = utils.get_max_family_members(ctx) async with ctx.typing(): family_member_count = 0 for i in author_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 for i in target_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 if family_member_count >= max_family_members: await lock.unlock() return await ctx.send( f"If you added {target.mention} to your family, you'd have over {max_family_members} in your family. Sorry!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Set up the proposal try: result = await utils.send_proposal_message( ctx, target, f"Hey, {target.mention}, {ctx.author.mention} wants to be your child! What do you think?", allow_bots=True, ) except Exception: result = None if result is None: return await lock.unlock() # Database it up async with self.bot.database() as db: try: await db( """INSERT INTO parents (parent_id, child_id, guild_id, timestamp) VALUES ($1, $2, $3, $4)""", target.id, ctx.author.id, family_guild_id, dt.utcnow(), ) except asyncpg.UniqueViolationError: await lock.unlock() return await result.ctx.send( "I ran into an error saving your family data - please try again later." ) await result.ctx.send( f"I'm happy to introduce {ctx.author.mention} as your child, {target.mention}!", wait=False, ) # And we're done target_tree._children.append(author_tree.id) author_tree._parent = target.id await re.publish('TreeMemberUpdate', author_tree.to_json()) await re.publish('TreeMemberUpdate', target_tree.to_json()) await re.disconnect() await lock.unlock()
async def currency_create(self, ctx: utils.Context): """ Add a new currency to your guild. """ # Make sure they only have 3 currencies already async with self.bot.database() as db: currency_rows = await db("""SELECT * FROM guild_currencies WHERE guild_id=$1""", ctx.guild.id) if len(currency_rows) >= self.MAX_GUILD_CURRENCIES: return await ctx.send(f"You can only have **{self.MAX_GUILD_CURRENCIES}** currencies per guild.") boolean_emojis = ["\N{HEAVY CHECK MARK}", "\N{HEAVY MULTIPLICATION X}"] # Set up the wait_for check here because we're gonna use it multiple times def check(message): return all([ message.channel.id == ctx.channel.id, message.author.id == ctx.author.id, ]) def reaction_check(message): def wrapper(payload): return all([ payload.message_id == message.id, payload.user_id == ctx.author.id, str(payload.emoji) in boolean_emojis, ]) return wrapper # Ask what they want the name of the currency to be await ctx.send("""What do you want the _name_ of the currency to be? Examples: "dollars", "pounds", "krona", etc.""") for _ in range(3): try: currency_name_message = await self.bot.wait_for("message", check=check, timeout=60) assert currency_name_message.content except asyncio.TimeoutError: return await ctx.send("Timed out on adding a new currency to the guild.") except AssertionError: await currency_name_message.reply("This isn't a valid currency name - please provide another one.") continue # Check that their provided name is valid async with self.bot.database() as db: check_rows = await db( """SELECT * FROM guild_currencies WHERE guild_id=$1 AND LOWER(currency_name)=LOWER($2)""", ctx.guild.id, currency_name_message.content, ) if check_rows: await currency_name_message.reply( f"You're already using a currency with the name **{currency_name_message.content}** - please provide another one.", allowed_mentions=discord.AllowedMentions.none(), ) continue break else: return await ctx.send("You failed giving a valid currency name too many times - please try again later.") # Ask what they want the short form of the currency to be await ctx.send("""What do you want the _short form_ of the currency to be? Examples: "USD", "GBP", "RS3", etc.""") for _ in range(3): try: currency_short_message = await self.bot.wait_for("message", check=check, timeout=60) assert currency_short_message.content break except asyncio.TimeoutError: return await ctx.send("Timed out on adding a new currency to the guild.") except AssertionError: await currency_short_message.reply("This isn't a valid currency name - please provide another one.") # Check that their provided name is valid async with self.bot.database() as db: check_rows = await db( """SELECT * FROM guild_currencies WHERE guild_id=$1 AND LOWER(short_form)=LOWER($2)""", ctx.guild.id, currency_short_message.content, ) if check_rows: await currency_name_message.reply( f"You're already using a currency with the short name **{currency_name_message.content}** - please provide another one.", allowed_mentions=discord.AllowedMentions.none(), ) continue break else: return await ctx.send("You failed giving a valid currency short name too many times - please try again later.") # Ask if we should add a daily command m = await ctx.send("""Do you want there to be a "daily" command available for this currency, where users can get between 9k and 13k every day?""") for e in boolean_emojis: self.bot.loop.create_task(m.add_reaction(e)) try: currency_daily_payload = await self.bot.wait_for("raw_reaction_add", check=reaction_check(m), timeout=60) except asyncio.TimeoutError: return await ctx.send("Timed out on adding a new currency to the guild.") # Add the new currency to the server async with ctx.typing(): async with self.bot.database() as db: await db( """INSERT INTO guild_currencies (guild_id, currency_name, short_form, allow_daily_command) VALUES ($1, $2, $3, $4)""", ctx.guild.id, currency_name_message.content, currency_short_message.content, str(currency_daily_payload.emoji) == "\N{HEAVY CHECK MARK}", ) return await ctx.send("Added a new currency to your server!")
async def shop(self, ctx:utils.Context): """ Shows you the available plants. """ # Get data from the user async with self.bot.database() as db: user_rows = await db("SELECT * FROM user_settings WHERE user_id=$1", ctx.author.id) plant_level_rows = await db("SELECT * FROM plant_levels WHERE user_id=$1", ctx.author.id) if user_rows: user_experience = user_rows[0]['user_experience'] plant_limit = user_rows[0]['plant_limit'] last_plant_shop_time = user_rows[0]['last_plant_shop_time'] or dt(2000, 1, 1) else: user_experience = 0 plant_limit = 1 last_plant_shop_time = dt(2000, 1, 1) can_purchase_new_plants = dt.utcnow() > last_plant_shop_time + timedelta(**self.bot.config.get('plants', {}).get('water_cooldown', {'minutes': 15})) buy_plant_cooldown_delta = None if can_purchase_new_plants is False: buy_plant_cooldown_delta = utils.TimeValue( ((last_plant_shop_time + timedelta(**self.bot.config.get('plants', {}).get('water_cooldown', {'minutes': 15}))) - dt.utcnow()).total_seconds() ) # Set up our initial items available_item_count = 0 # Used to make sure we can continue the command embed = utils.Embed(use_random_colour=True, description="") ctx._set_footer(embed) # See what we wanna get to doing embed.description += ( f"What would you like to spend your experience to buy, {ctx.author.mention}? " f"You currently have **{user_experience:,} exp**, and you're using {len(plant_level_rows):,} of your {plant_limit:,} available plant pots.\n" ) available_plants = await self.get_available_plants(ctx.author.id) # Add "can't purchase new plant" to the embed if can_purchase_new_plants is False: embed.description += f"\nYou can't purchase new plants for another **{buy_plant_cooldown_delta.clean}**.\n" # Add plants to the embed plant_text = [] for plant in sorted(available_plants.values()): modifier = lambda x: x text = f"{plant.display_name.capitalize()} - `{plant.required_experience:,} exp`" if can_purchase_new_plants and plant.required_experience <= user_experience and len(plant_level_rows) < plant_limit: available_item_count += 1 else: modifier = strikethrough plant_text.append(modifier(text)) # Say when the plants will change now = dt.utcnow() remaining_time = utils.TimeValue((dt(now.year if now.month < 12 else now.year + 1, now.month + 1 if now.month < 12 else 1, 1) - now).total_seconds()) plant_text.append(f"These plants will change in {remaining_time.clean_spaced}.") embed.add_field("Available Plants", '\n'.join(plant_text), inline=True) # Set up items to be added to the embed item_text = [] # Add pots modifier = lambda x: x text = f"Pot - `{self.get_points_for_plant_pot(plant_limit):,} exp`" if user_experience >= self.get_points_for_plant_pot(plant_limit) and plant_limit < self.bot.config.get('plants', {}).get('hard_plant_cap', 10): available_item_count += 1 else: modifier = strikethrough item_text.append(modifier(text)) # Add variable items for item in self.bot.items.values(): modifier = lambda x: x text = f"{item.display_name.capitalize()} - `{item.price:,} exp`" if user_experience >= item.price: available_item_count += 1 else: modifier = strikethrough item_text.append(modifier(text)) # Add all our items to the embed embed.add_field("Available Items", '\n'.join(item_text), inline=True) # Cancel if they don't have anything available if available_item_count == 0: embed.description += "\n**There is currently nothing available which you can purchase.**\n" return await ctx.send(embed=embed) else: embed.description += "\n**Say the name of the item you want to purchase, or type `cancel` to exit the shop with nothing.**\n" # Wait for them to respond shop_menu_message = await ctx.send(embed=embed) try: done, pending = await asyncio.wait([ self.bot.wait_for("message", check=lambda m: m.author.id == ctx.author.id and m.channel == ctx.channel and m.content), self.bot.wait_for("raw_message_delete", check=lambda m: m.message_id == shop_menu_message.id), ], timeout=120, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError: pass # See how they responded for future in pending: future.cancel() try: done = done.pop().result() except KeyError: return await ctx.send(f"Timed out asking for plant type {ctx.author.mention}.") if isinstance(done, discord.RawMessageDeleteEvent): return plant_type_message = done given_response = plant_type_message.content.lower().replace(' ', '_') # See if they want to cancel if given_response == "cancel": try: await plant_type_message.add_reaction("\N{OK HAND SIGN}") except discord.HTTPException: pass return # See if they want a plant pot if given_response == "pot": if plant_limit >= self.bot.config.get('plants', {}).get('hard_plant_cap', 10): return await ctx.send(f"You're already at the maximum amount of pots, {ctx.author.mention}! :c") if user_experience >= self.get_points_for_plant_pot(plant_limit): async with self.bot.database() as db: await db( """INSERT INTO user_settings (user_id, plant_limit, user_experience) VALUES ($1, 2, $2) ON CONFLICT (user_id) DO UPDATE SET plant_limit=user_settings.plant_limit+1, user_experience=user_settings.user_experience-excluded.user_experience""", ctx.author.id, self.get_points_for_plant_pot(plant_limit) ) return await ctx.send(f"Given you another plant pot, {ctx.author.mention}!") else: return await ctx.send(f"You don't have the required experience to get a new plant pot, {ctx.author.mention} :c") # See if they want a revival token item_type = self.bot.items.get(given_response) if item_type is not None: if user_experience >= item_type.price: async with self.bot.database() as db: await db.start_transaction() await db( """INSERT INTO user_settings (user_id, user_experience) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET user_experience=user_settings.user_experience-excluded.user_experience""", ctx.author.id, item_type.price ) await db( """INSERT INTO user_inventory (user_id, item_name, amount) VALUES ($1, $2, 1) ON CONFLICT (user_id, item_name) DO UPDATE SET amount=user_inventory.amount+excluded.amount""", ctx.author.id, item_type.name ) await db.commit_transaction() return await ctx.send(f"Given you a **{item_type.display_name}**, {ctx.author.mention}!") else: return await ctx.send(f"You don't have the required experience to get a **{item_type.display_name}**, {ctx.author.mention} :c") # See if they want a plant try: plant_type = self.bot.plants[given_response] except KeyError: return await ctx.send(f"`{plant_type_message.content}` isn't an available plant name, {ctx.author.mention}!", allowed_mentions=discord.AllowedMentions(users=[ctx.author], roles=False, everyone=False)) if can_purchase_new_plants is False: return await ctx.send(f"You can't purchase new plants for another **{buy_plant_cooldown_delta.clean}**.") if plant_type not in available_plants.values(): return await ctx.send(f"**{plant_type.display_name.capitalize()}** isn't available in your shop this month, {ctx.author.mention} :c") if plant_type.required_experience > user_experience: return await ctx.send(f"You don't have the required experience to get a **{plant_type.display_name}**, {ctx.author.mention} (it requires {plant_type.required_experience}, you have {user_experience}) :c") if len(plant_level_rows) >= plant_limit: return await ctx.send(f"You don't have enough plant pots to be able to get a **{plant_type.display_name}**, {ctx.author.mention} :c") # Get a name for the plant await ctx.send("What name do you want to give your plant?") while True: try: plant_name_message = await self.bot.wait_for("message", check=lambda m: m.author.id == ctx.author.id and m.channel == ctx.channel and m.content, timeout=120) except asyncio.TimeoutError: return await ctx.send(f"Timed out asking for plant name {ctx.author.mention}.") plant_name = self.bot.get_cog("PlantCareCommands").validate_name(plant_name_message.content) if len(plant_name) > 50 or len(plant_name) == 0: await ctx.send("That name is too long! Please give another one instead!") else: break # Save the enw plant to database async with self.bot.database() as db: plant_name_exists = await db( "SELECT * FROM plant_levels WHERE user_id=$1 AND LOWER(plant_name)=LOWER($2)", ctx.author.id, plant_name ) if plant_name_exists: return await ctx.send(f"You've already used the name `{plant_name}` for one of your other plants - please run this command again to give a new one!", allowed_mentions=discord.AllowedMentions(everyone=False, users=False, roles=False)) await db( """INSERT INTO plant_levels (user_id, plant_name, plant_type, plant_nourishment, last_water_time, original_owner_id, plant_adoption_time, plant_pot_hue) VALUES ($1::BIGINT, $2, $3, 0, $4, $1::BIGINT, TIMEZONE('UTC', NOW()), CAST($1::BIGINT % 360 AS SMALLINT)) ON CONFLICT (user_id, plant_name) DO UPDATE SET plant_nourishment=0, last_water_time=$4""", ctx.author.id, plant_name, plant_type.name, dt(2000, 1, 1), ) await db( "UPDATE user_settings SET user_experience=user_settings.user_experience-$2, last_plant_shop_time=TIMEZONE('UTC', NOW()) WHERE user_id=$1", ctx.author.id, plant_type.required_experience, ) await ctx.send(f"Planted your **{plant_type.display_name}** seeds!")
async def edittemplate(self, ctx: utils.Context, template: localutils.Template): """ Edits a template for your guild. """ # See if they're already editing that template if self.template_editing_locks[ctx.guild.id].locked(): return await ctx.send("You're already editing a template.") # See if they're bot support is_bot_support = await self.user_is_bot_support(ctx) # Grab the template edit lock async with self.template_editing_locks[ctx.guild.id]: # Get the template fields async with self.bot.database() as db: await template.fetch_fields(db) guild_settings_rows = await db( """SELECT * FROM guild_settings WHERE guild_id=$1 OR guild_id=0 ORDER BY guild_id DESC""", ctx.guild.id, ) perks = await localutils.get_perks_for_guild(db, ctx.guild.id) ctx.guild_perks = perks guild_settings = guild_settings_rows[0] # Set up our initial vars so we can edit them later template_display_edit_message = await ctx.send( "Loading template...") # The message with the template components = utils.MessageComponents.add_buttons_with_rows( utils.Button("Template name", custom_id="1\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Profile verification channel", custom_id="2\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Profile archive channel", custom_id="3\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Profile completion role", custom_id="4\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Template fields", custom_id="5\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Profile count per user", custom_id="6\N{COMBINING ENCLOSING KEYCAP}"), utils.Button("Done", custom_id="DONE", style=utils.ButtonStyle.SUCCESS), ) template_options_edit_message = None should_edit = True # Whether or not the template display message should be edited # Start our edit loop while True: # Ask what they want to edit if should_edit: try: await template_display_edit_message.edit( content=None, embed=template.build_embed(self.bot, brief=True), allowed_mentions=discord.AllowedMentions( roles=False), ) except discord.HTTPException: return should_edit = False # Wait for a response from the user try: if template_options_edit_message: await template_options_edit_message.edit( components=components.enable_components()) else: template_options_edit_message = await ctx.send( "What would you like to edit?", components=components) payload = await template_options_edit_message.wait_for_button_click( check=lambda p: p.user.id == ctx.author.id, timeout=120) await payload.ack() reaction = payload.component.custom_id except asyncio.TimeoutError: try: await template_options_edit_message.edit( content="Timed out waiting for edit response.", components=None) except discord.HTTPException: pass return # See what they reacted with try: available_reactions = { "1\N{COMBINING ENCLOSING KEYCAP}": ("name", str), "2\N{COMBINING ENCLOSING KEYCAP}": ("verification_channel_id", commands.TextChannelConverter()), "3\N{COMBINING ENCLOSING KEYCAP}": ("archive_channel_id", commands.TextChannelConverter()), "4\N{COMBINING ENCLOSING KEYCAP}": ("role_id", commands.RoleConverter()), "5\N{COMBINING ENCLOSING KEYCAP}": (None, self.edit_field(ctx, template, guild_settings, is_bot_support)), "6\N{COMBINING ENCLOSING KEYCAP}": ("max_profile_count", int), "DONE": None, } attr, converter = available_reactions[reaction] except TypeError: break # They're done # Disable the components await template_options_edit_message.edit( components=components.disable_components()) # If they want to edit a field, we go through this section if attr is None: # Let them change the fields fields_have_changed = await converter # If the fields have changed then we should update the message if fields_have_changed: async with self.bot.database() as db: await template.fetch_fields(db) should_edit = True # And we're done with this round, so continue upwards continue # Change the given attribute should_edit = await self.change_template_attribute( ctx, template, guild_settings, is_bot_support, attr, converter) # Tell them it's done await template_options_edit_message.edit( content= (f"Finished editing template. Users can create profiles with `{ctx.clean_prefix}{template.name.lower()} set`, " f"edit with `{ctx.clean_prefix}{template.name.lower()} edit`, and show them with " f"`{ctx.clean_prefix}{template.name.lower()} get`."), components=None, )
async def leaderboard(self, ctx: utils.Context, days: int = None): """ Gives you the leaderboard users for the server. """ if days is None: days = self.bot.guild_settings[ ctx.guild.id]['activity_window_days'] elif days <= 0: days = 7 elif days > 365: days = 365 # This takes a while async with ctx.typing(): # Get all their valid user IDs async with self.bot.database() as db: message_rows = await db( """SELECT user_id, COUNT(timestamp) FROM user_messages WHERE guild_id=$1 AND timestamp > TIMEZONE('UTC', NOW()) - MAKE_INTERVAL(days => $2) GROUP BY user_id ORDER BY COUNT(timestamp) DESC;""", ctx.guild.id, days, ) vc_rows = await db( """SELECT user_id, COUNT(timestamp) FROM user_vc_activity WHERE guild_id=$1 AND timestamp > TIMEZONE('UTC', NOW()) - MAKE_INTERVAL(days => $2) GROUP BY user_id ORDER BY COUNT(timestamp) DESC;""", ctx.guild.id, days, ) if self.bot.guild_settings[ ctx.guild.id]['minecraft_srv_authorization']: minecraft_rows = await db( """SELECT user_id, COUNT(timestamp) FROM minecraft_server_activity WHERE guild_id=$1 AND timestamp > TIMEZONE('UTC', NOW()) - MAKE_INTERVAL(days => $2) GROUP BY user_id ORDER BY COUNT(timestamp) DESC;""", ctx.guild.id, days, ) else: minecraft_rows = [] # Sort that into more formattable data user_data_dict = collections.defaultdict({ 'message_count': 0, 'vc_minute_count': 0, 'minecraft_minute_count': 0 }.copy) # uid: {message_count: int, vc_minute_count: int} for row in message_rows: user_data_dict[row['user_id']]['message_count'] = row['count'] for row in vc_rows: user_data_dict[ row['user_id']]['vc_minute_count'] = row['count'] for row in minecraft_rows: user_data_dict[ row['user_id']]['minecraft_minute_count'] = row['count'] # And now make it into something we can sort valid_guild_user_data = [{ 'id': uid, 'm': d['message_count'], 'vc': d['vc_minute_count'], 'mc': d['minecraft_minute_count'] } for uid, d in user_data_dict.items() if ctx.guild.get_member(uid)] ordered_guild_user_data = sorted(valid_guild_user_data, key=lambda k: k['m'] + (k['vc'] // 5) + (k['mc'] // 5), reverse=True) # And now make it into strings ordered_guild_user_strings = [] for d in ordered_guild_user_data: total_points = d['m'] + (d['vc'] // 5) + (d['mc'] // 5) vc_time = utils.TimeValue(d['vc'] * 60).clean_spaced or '0m' if self.bot.guild_settings[ ctx.guild.id]['minecraft_srv_authorization']: ordered_guild_user_strings.append( f"**<@{d['id']}>** - **{total_points:,}** (**{d['m']:,}** text, **{vc_time}** VC, **{d['mc']:,}** Minecraft)" ) else: ordered_guild_user_strings.append( f"**<@{d['id']}>** - **{total_points:,}** (**{d['m']:,}** text, **{vc_time}** VC)" ) # Make menu return await utils.Paginator( ordered_guild_user_strings, formatter=utils.Paginator.default_ranked_list_formatter).start(ctx)
async def marry(self, ctx: vbu.Context, *, target: utils.converters.UnblockedMember): """ Lets you propose to another Discord user. """ # Get the family tree member objects family_guild_id = utils.get_family_guild_id(ctx) author_tree, target_tree = utils.FamilyTreeMember.get_multiple(ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not themselves if target.id == ctx.author.id: return await ctx.send("That's you. You can't marry yourself.", wait=False) # Check they're not a bot if target.bot: if target.id == self.bot.user.id: return await ctx.send("I think I could do better actually, but thank you!", wait=False) return await ctx.send("That is a robot. Robots cannot consent to marriage.", wait=False) # Lock those users re = await self.bot.redis.get_connection() try: lock = await utils.ProposalLock.lock(re, ctx.author.id, target.id) except utils.ProposalInProgress: return await ctx.send("Aren't you popular! One of you is already waiting on a proposal - please try again later.", wait=False) # See if we're already married if author_tree._partner: await lock.unlock() return await ctx.send( f"Hey, {ctx.author.mention}, you're already married! Try divorcing your partner first \N{FACE WITH ROLLING EYES}", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # See if the *target* is already married if target_tree._partner: await lock.unlock() return await ctx.send( f"Sorry, {ctx.author.mention}, it looks like {target.mention} is already married \N{PENSIVE FACE}", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # See if they're already related async with ctx.typing(): relation = author_tree.get_relation(target_tree) if relation and utils.guild_allows_incest(ctx) is False: await lock.unlock() return await ctx.send( f"Woah woah woah, it looks like you guys are already related! {target.mention} is your {relation}!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Check the size of their trees # TODO I can make this a util because I'm going to use it a couple times max_family_members = utils.get_max_family_members(ctx) async with ctx.typing(): family_member_count = 0 for i in author_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 for i in target_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 if family_member_count >= max_family_members: await lock.unlock() return await ctx.send( f"If you added {target.mention} to your family, you'd have over {max_family_members} in your family. Sorry!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Set up the proposal try: result = await utils.send_proposal_message( ctx, target, f"Hey, {target.mention}, it would make {ctx.author.mention} really happy if you would marry them. What do you say?", ) except Exception: result = None if result is None: return await lock.unlock() # They said yes! async with self.bot.database() as db: try: await db.start_transaction() await db( "INSERT INTO marriages (user_id, partner_id, guild_id, timestamp) VALUES ($1, $2, $3, $4), ($2, $1, $3, $4)", ctx.author.id, target.id, family_guild_id, dt.utcnow(), ) await db.commit_transaction() except asyncpg.UniqueViolationError: await lock.unlock() return await result.ctx.send("I ran into an error saving your family data.", wait=False) await result.ctx.send( f"I'm happy to introduce {target.mention} into the family of {ctx.author.mention}!", wait=False, ) # Keep allowed mentions on # Ping over redis author_tree._partner = target.id target_tree._partner = ctx.author.id await re.publish('TreeMemberUpdate', author_tree.to_json()) await re.publish('TreeMemberUpdate', target_tree.to_json()) await re.disconnect() await lock.unlock()
async def setsona(self, ctx: utils.Context): """ Stores your fursona information in the bot. """ # See if the user already has a fursona stored async with self.bot.database() as db: rows = await db( "SELECT * FROM fursonas WHERE guild_id=$1 AND user_id=$2", ctx.guild.id, ctx.author.id) current_sona_names = [row['name'].lower() for row in rows] ctx.current_sona_names = current_sona_names # See if they're at the limit try: sona_limit = max(o for i, o in self.bot.guild_settings[ ctx.guild.id].setdefault('role_sona_count', dict()).items() if int(i) in ctx.author._roles) except ValueError: sona_limit = 1 if len(current_sona_names) >= sona_limit: return await ctx.send( "You're already at the sona limit - you have to delete one to be able to set another." ) # See if they're setting one up already if ctx.author.id in self.currently_setting_sonas: return await ctx.send( "You're already setting up a sona! Please finish that one off first!" ) # Try and send them an initial DM user = ctx.author start_message = f"Now taking you through setting up your sona on **{ctx.guild.name}**!\n" if not self.bot.guild_settings[ctx.guild.id]["nsfw_is_allowed"]: start_message += f"NSFW fursonas are not allowed for **{ctx.guild.name}** and will be automatically declined.\n" try: await user.send(start_message.strip()) except discord.Forbidden: return await ctx.send( "I couldn't send you a DM! Please open your DMs for this server and try again." ) self.currently_setting_sonas.add(user.id) await ctx.send("Sent you a DM!") # Ask about name name_message = await self.send_verification_message( user, "What is the name of your sona?") if name_message is None: return self.currently_setting_sonas.remove(user.id) if name_message.content.lower() in current_sona_names: self.currently_setting_sonas.remove(user.id) return await user.send( f"You already have a sona with the name `{name_message.content}`. Please start your setup again and provide a different name." ) # Ask about gender gender_message = await self.send_verification_message( user, "What's your sona's gender?") if gender_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about age age_message = await self.send_verification_message( user, "How old is your sona?") if age_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about species species_message = await self.send_verification_message( user, "What species is your sona?") if species_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about orientation orientation_message = await self.send_verification_message( user, "What's your sona's orientation?") if orientation_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about height height_message = await self.send_verification_message( user, "How tall is your sona?") if height_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about weight weight_message = await self.send_verification_message( user, "What's the weight of your sona?") if weight_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about bio bio_message = await self.send_verification_message( user, "What's the bio of your sona?", max_length=1000) if bio_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about image def check(m) -> bool: return all([ isinstance(m.channel, discord.DMChannel), m.author.id == user.id, any([ m.content.lower() == "no", self.get_image_from_message(m) ]), ]) image_message = await self.send_verification_message( user, "Do you have an image for your sona? Please post it if you have one (as a link or an attachment), or say `no` to continue without.", check=check) if image_message is None: return self.currently_setting_sonas.remove(user.id) # Ask about NSFW if self.bot.guild_settings[ctx.guild.id]["nsfw_is_allowed"]: check = lambda m: isinstance( m.channel, discord.DMChannel ) and m.author.id == user.id and m.content.lower( ) in ["yes", "no"] nsfw_message = await self.send_verification_message( user, "Is your sona NSFW? Please either say `yes` or `no`.", check=check) if nsfw_message is None: return self.currently_setting_sonas.remove(user.id) else: nsfw_content = nsfw_message.content.lower() else: nsfw_content = "no" # Format that into data image_content = None if image_message.content.lower( ) == "no" else self.get_image_from_message(image_message) information = { 'name': name_message.content, 'gender': gender_message.content, 'age': age_message.content, 'species': species_message.content, 'orientation': orientation_message.content, 'height': height_message.content, 'weight': weight_message.content, 'bio': bio_message.content, 'image': image_content, 'nsfw': nsfw_content == "yes", } self.currently_setting_sonas.remove(user.id) ctx.information = information if information['nsfw'] and not self.bot.guild_settings[ ctx.guild.id]["nsfw_is_allowed"]: return await user.send( "Your fursona has been automatically declined as it is NSFW") await self.bot.get_command("setsonabyjson").invoke(ctx)
async def importsona(self, ctx: utils.Context): """ Get your sona from another server. """ # See if they're setting one up already if ctx.author.id in self.currently_setting_sonas: return await ctx.send( "You're already setting up a sona! Please finish that one off first!" ) # Try and send them an initial DM try: await ctx.author.send( f"Now taking you through importing your sona to **{ctx.guild.name}**!" ) except discord.Forbidden: return await ctx.send( "I couldn't send you a DM! Please open your DMs for this server and try again." ) self.currently_setting_sonas.add(ctx.author.id) await ctx.send("Sent you a DM!") # Get sona data async with self.bot.database() as db: database_rows = await db("SELECT * FROM fursonas WHERE user_id=$1", ctx.author.id) # Format that into a list all_user_sonas = [] for row in database_rows: try: guild = self.bot.get_guild( row['guild_id']) or await self.bot.fetch_guild( row['guild_id']) except discord.Forbidden: guild = None # Add to the all sona list menu_data = dict(row) if guild: menu_data.update({"guild_name": guild.name}) else: menu_data.update({"guild_name": "Unknown Guild Name"}) all_user_sonas.append(menu_data) # Let's add our other servers via their APIs for api_data in self.OTHER_FURRY_GUILD_DATA[::-1]: # Format data url = api_data['url'] params = { i: o.format(user=ctx.author, guild=ctx.guild, bot=self.bot) for i, o in api_data.get('params', dict()).copy().items() } headers = { i: o.format(user=ctx.author, guild=ctx.guild, bot=self.bot) for i, o in api_data.get('headers', dict()).copy().items() } # Run request try: async with self.bot.session.get(url, params=params, headers=headers) as r: grabbed_sona_data = await r.json() except Exception: grabbed_sona_data = {'data': []} # Add to lists if grabbed_sona_data['data']: guild_id = api_data['guild_id'] guild_name = api_data['name'] # Add to the all sona list for sona in grabbed_sona_data['data']: menu_data = sona.copy() menu_data.update({ "guild_name": guild_name, "guild_id": guild_id }) all_user_sonas.append(menu_data) # Filter the list all_user_sonas = [ i for i in all_user_sonas if i['guild_id'] != ctx.guild.id ] if not self.bot.guild_settings[ctx.guild.id]["nsfw_is_allowed"]: all_user_sonas = [i for i in all_user_sonas if i['nsfw'] is False] if not all_user_sonas: self.currently_setting_sonas.remove(ctx.author.id) return await ctx.send( "You have no sonas available to import from other servers.") # Send it off to the user pages = menus.MenuPages( source=FursonaPageSource(all_user_sonas, per_page=1)) await pages.start(ctx, channel=ctx.author, wait=True) # Ask if the user wants to import the sona they stopped on sona_data = pages.raw_sona_data ask_import_message = await ctx.author.send( f"Do you want to import your sona from **{sona_data['guild_name']}**?" ) await ask_import_message.add_reaction(self.CHECK_MARK_EMOJI) await ask_import_message.add_reaction(self.CROSS_MARK_EMOJI) try: check = lambda r, u: r.message.id == ask_import_message.id and u.id == ctx.author.id reaction, _ = await self.bot.wait_for("reaction_add", check=check, timeout=120) except asyncio.TimeoutError: self.currently_setting_sonas.remove(ctx.author.id) return await ctx.author.send("Timed out asking about sona import.") # Import data self.currently_setting_sonas.remove(ctx.author.id) emoji = str(reaction.emoji) if emoji == self.CROSS_MARK_EMOJI: return await ctx.author.send( "Alright, cancelled importing your sona.") command = self.bot.get_command("setsonabyjson") ctx.information = sona_data return await command.invoke(ctx)
async def herbiary(self, ctx: utils.Context, *, plant_name: str = None): """ Get the information for a given plant. """ # See if a name was given if plant_name is None: plant_dict = collections.defaultdict(list) for plant in self.bot.plants.values(): plant_dict[plant.plant_level].append( plant.display_name.capitalize()) embed_fields = [] embed = utils.Embed(use_random_colour=True) for plant_level, plants in plant_dict.items(): plants.sort() embed_fields.append( (f"Level {plant_level}", "\n".join(plants))) for field in sorted(embed_fields): embed.add_field(*field, inline=True) ctx._set_footer(embed) return await ctx.send(embed=embed) # See if the given name is valid plant_name = plant_name.replace(' ', '_').lower() if plant_name not in self.bot.plants: return await ctx.send( f"There's no plant with the name **{plant_name.replace('_', ' ')}** :c", allowed_mentions=discord.AllowedMentions(users=False, roles=False, everyone=False)) plant = self.bot.plants[plant_name] # Work out our artist info to be displayed description_list = [] artist_info = self.artist_info.get(plant.artist, {}).copy() discord_id = artist_info.pop('discord', None) description_list.append(f"**Artist `{plant.artist}`**") if discord_id: description_list.append( f"Discord: <@{discord_id}> (`{discord_id}`)") for i, o in sorted(artist_info.items()): description_list.append(f"{i.capitalize()}: [Link]({o})") description_list.append("") # Work out some other info we want displayed description_list.append(f"Plant level {plant.plant_level}") description_list.append(f"Costs {plant.required_experience} exp") description_list.append( f"Gives between {plant.experience_gain['minimum']} and {plant.experience_gain['maximum']} exp" ) # Embed the data with utils.Embed(use_random_colour=True) as embed: embed.title = plant.display_name.capitalize() embed.description = '\n'.join(description_list) embed.set_image("attachment://plant.png") ctx._set_footer(embed) display_utils = self.bot.get_cog("PlantDisplayUtils") plant_image_bytes = display_utils.image_to_bytes( display_utils.get_plant_image(plant.name, 21, "clay", random.randint(0, 360))) await ctx.send(embed=embed, file=discord.File(plant_image_bytes, filename="plant.png"))
async def waterplant(self, ctx: utils.Context, *, plant_name: str): """ Increase the growth level of your plant. """ # Decide on our plant type - will be ignored if there's already a plant db = await self.bot.database.get_connection() # See if they have a plant available plant_level_row = await db( "SELECT * FROM plant_levels WHERE user_id=$1 AND LOWER(plant_name)=LOWER($2)", ctx.author.id, plant_name) if not plant_level_row: await db.disconnect() return await ctx.send( f"You don't have a plant with the name **{plant_name}**! Run `{ctx.prefix}getplant` to plant some new seeds, or `{ctx.prefix}plants` to see the list of plants you have already!", allowed_mentions=discord.AllowedMentions(users=False, roles=False, everyone=False)) plant_data = self.bot.plants[plant_level_row[0]['plant_type']] # See if they're allowed to water things if plant_level_row[0]['last_water_time'] + timedelta( **self.bot.config.get('plants', {}).get( 'water_cooldown', {'minutes': 15})) > dt.utcnow( ) and ctx.author.id not in self.bot.owner_ids: await db.disconnect() timeout = utils.TimeValue( ((plant_level_row[0]['last_water_time'] + timedelta(**self.bot.config.get('plants', {}).get( 'water_cooldown', {'minutes': 15}))) - dt.utcnow()).total_seconds()) return await ctx.send( f"You need to wait another {timeout.clean_spaced} to be able water your {plant_level_row[0]['plant_type'].replace('_', ' ')}." ) last_water_time = plant_level_row[0]['last_water_time'] # See if the plant should be dead if plant_level_row[0]['plant_nourishment'] < 0: plant_level_row = await db( """UPDATE plant_levels SET plant_nourishment=LEAST(-plant_levels.plant_nourishment, plant_levels.plant_nourishment), last_water_time=$3 WHERE user_id=$1 AND LOWER(plant_name)=LOWER($2) RETURNING *""", ctx.author.id, plant_name, dt.utcnow(), ) # Increase the nourishment otherwise else: plant_level_row = await db( """UPDATE plant_levels SET plant_nourishment=LEAST(plant_levels.plant_nourishment+1, $4), last_water_time=$3 WHERE user_id=$1 AND LOWER(plant_name)=LOWER($2) RETURNING *""", ctx.author.id, plant_name, dt.utcnow(), plant_data.max_nourishment_level, ) # Add to the user exp if the plant is alive user_plant_data = plant_level_row[0] gained_experience = 0 original_gained_experience = 0 multipliers = [] # List[Tuple[float, "reason"]] additional_text = [] # List[str] voted_on_topgg = False # And now let's water the damn thing if user_plant_data['plant_nourishment'] > 0: # Get the experience that they should have gained gained_experience = plant_data.get_experience() original_gained_experience = gained_experience # See if we want to give them a 30 second water-time bonus if dt.utcnow() - last_water_time - timedelta( **self.bot.config.get('plants', {}).get( 'water_cooldown', {'minutes': 15})) <= timedelta( seconds=30): multipliers.append(( 1.5, "You watered within 30 seconds of your plant's cooldown resetting." )) # See if we want to give the new owner bonus if plant_level_row[0]['user_id'] != plant_level_row[0][ 'original_owner_id']: multipliers.append( (1.05, "You watered a plant that you got from a trade.")) # See if we want to give them the voter bonus if self.bot.config.get( 'bot_listing_api_keys', {}).get('topgg_token') and await self.get_user_voted( ctx.author.id): multipliers.append(( 1.1, f"You [voted for the bot](https://top.gg/bot/{self.bot.user.id}/vote) on Top.gg." )) voted_on_topgg = True # See if we want to give them the plant longevity bonus if user_plant_data['plant_adoption_time'] < dt.utcnow( ) - timedelta(days=7): multipliers.append( (1.2, "Your plant has been alive for longer than a week.")) # Add the actual multiplier values for multiplier, _ in multipliers: gained_experience *= multiplier # Update db gained_experience = int(gained_experience) await db( """INSERT INTO user_settings (user_id, user_experience) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET user_experience=user_settings.user_experience+$2""", ctx.author.id, gained_experience, ) # Send an output await db.disconnect() if user_plant_data['plant_nourishment'] < 0: return await ctx.send( "You sadly pour water into the dry soil of your silently wilting plant :c" ) # Set up our output text gained_exp_string = f"**{gained_experience}**" if gained_experience == original_gained_experience else f"~~{original_gained_experience}~~ **{gained_experience}**" output_lines = [] if plant_data.get_nourishment_display_level( user_plant_data['plant_nourishment'] ) > plant_data.get_nourishment_display_level( user_plant_data['plant_nourishment'] - 1): output_lines.append( f"You gently pour water into **{plant_level_row[0]['plant_name']}**'s soil, gaining you {gained_exp_string} experience, watching your plant grow!~" ) else: output_lines.append( f"You gently pour water into **{plant_level_row[0]['plant_name']}**'s soil, gaining you {gained_exp_string} experience~" ) for m, t in multipliers: output_lines.append(f"**{m}x**: {t}") for t in additional_text: output_lines.append(t) # Try and embed the message embed = None if ctx.guild is None or ctx.channel.permissions_for( ctx.guild.me).embed_links: # Make initial embed embed = utils.Embed(use_random_colour=True, description=output_lines[0]) # Add multipliers if len(output_lines) > 1: embed.add_field("Multipliers", "\n".join( [i.strip('') for i in output_lines[1:]]), inline=False) # Add "please vote for Flower" footer counter = 0 ctx._set_footer(embed) check = lambda text: 'vote' in text if voted_on_topgg else False # Return True to change again - force to "vote for flower" if they haven't voted, else anything but while counter < 100 and check(embed.footer.text.lower()): ctx._set_footer(embed) counter += 1 # Clear the text we would otherwise output output_lines.clear() # Send message return await ctx.send("\n".join(output_lines), embed=embed)