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", user.id) # See if they have anything available plant_names = sorted([(i['plant_name'], i['plant_type'], i['plant_nourishment']) for i in user_rows]) if not plant_names: 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") for i in plant_names: if i[2] >= 0: embed.add_field( i[0], f"{i[1].replace('_', ' ')}, nourishment level {i[2]}/{self.bot.plants[i[1]].max_nourishment_level}" ) else: embed.add_field(i[0], f"{i[1].replace('_', ' ')}, dead :c") # Return to user return await ctx.send(embed=embed)
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: with utils.Embed(use_random_colour=True) as embed: plants = sorted(self.bot.plants.values(), key=lambda x: (x.plant_level, x.name)) embed.description = '\n'.join([ f"**{i.display_name.capitalize()}** - level {i.plant_level}" for i in plants ]) 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[plant.artist].copy() discord_id = artist_info.pop('discord', None) if discord_id: description_list.append(f"**Artist `{plant.artist}`**") description_list.append( f"Discord: <@{discord_id}> (`{discord_id}`)") else: description_list.append(f"**Artist `{plant.artist}`**") 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") display_cog = self.bot.get_cog("PlantDisplayCommands") plant_image_bytes = display_cog.image_to_bytes( display_cog.get_plant_image(plant.name, 0, 21, "clay", random.randint(0, 360))) await ctx.send(embed=embed, file=discord.File(plant_image_bytes, filename="plant.png"))
async def giveaway(self, ctx: utils.Context, duration: utils.TimeValue, winner_count: int, *, giveaway_name: str): """Start a giveaway""" # Validate winner count if winner_count < 1: return await ctx.send( "You need to give a winner count larger than 0.") if winner_count > 200: return await ctx.send("You can't give more than 200 winners.") # Create the embed with utils.Embed(colour=0x00ff00) as embed: embed.title = giveaway_name embed.description = "Click the reaction below to enter!" embed.set_footer(text=f"{winner_count} winners | Ends at") embed.timestamp = dt.utcnow() + duration.delta giveaway_message = await ctx.send(embed=embed) # Add to database async with self.bot.database() as db: await db( """INSERT INTO giveaways (channel_id, message_id, winner_count, ending_time, description) VALUES ($1, $2, $3, $4, $5)""", giveaway_message.channel.id, giveaway_message.id, winner_count, dt.utcnow() + duration.delta, giveaway_name, ) # Add reaction await giveaway_message.add_reaction("\N{PARTY POPPER}")
async def stats(self, ctx: utils.Context): """Gives you the stats for the bot""" # Get creator info creator_id = self.bot.config["owners"][0] creator = self.bot.get_user(creator_id) or await self.bot.fetch_user( creator_id) # Make embed with utils.Embed(colour=0x1e90ff) as embed: embed.set_footer(str(self.bot.user), icon_url=self.bot.user.avatar_url) embed.add_field("Creator", f"{creator!s}\n{creator_id}") embed.add_field("Library", f"Discord.py {discord.__version__}") if self.bot.shard_count != len(self.bot.shard_ids): embed.add_field( "Average Guild Count", int((len(self.bot.guilds) / len(self.bot.shard_ids)) * self.bot.shard_count)) else: embed.add_field("Guild Count", len(self.bot.guilds)) embed.add_field("Shard Count", self.bot.shard_count) embed.add_field("Average WS Latency", f"{(self.bot.latency * 1000):.2f}ms") embed.add_field( "Coroutines", f"{len([i for i in asyncio.Task.all_tasks() if not i.done()])} running, {len(asyncio.Task.all_tasks())} total." ) # Send it out wew let's go await ctx.send(embed=embed)
async def bongdist(self, ctx: utils.Context, user: discord.Member = None): """Gives you the bong leaderboard""" user = user or ctx.author async with self.bot.database() as db: rows = await db( "SELECT timestamp - message_timestamp AS reaction_time FROM bong_log WHERE user_id=$1 AND guild_id=$2", user.id, ctx.guild.id) if not rows: return await ctx.send( f"{user.mention} has reacted to the bong message yet on your server." ) # Build our output graph fig = plt.figure() ax = fig.subplots() bplot = ax.boxplot([i['reaction_time'].total_seconds() for i in rows], patch_artist=True) ax.axis([0, 2, 0, 10]) for i in bplot['boxes']: i.set_facecolor('lightblue') # Fix axies # ax.axis('off') ax.grid(True) # Tighten border fig.tight_layout() # Output to user baybeeee fig.savefig('activity.png', bbox_inches='tight', pad_inches=0) with utils.Embed() as embed: embed.set_image(url="attachment://activity.png") await ctx.send(embed=embed, file=discord.File("activity.png"))
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)) user_rows = await db( "SELECT * FROM user_settings WHERE user_id=$1", user.id) # Filter into variables if plant_rows and user_rows: display_data = self.get_display_data(plant_rows[0], user_rows[0]) elif plant_rows: display_data = self.get_display_data(plant_rows[0], None, user.id) elif user_rows: display_data = self.get_display_data(None, user_rows[0]) else: display_data = self.get_display_data(None, None, 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 = self.image_to_bytes(self.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") await ctx.send(embed=embed, file=file)
async def update_shop_message(self, guild: discord.Guild): """Edit the shop message to to be pretty good""" # Get the shop message self.logger.info(f"Shop message update dispatched (G{guild.id})") try: shop_channel = self.bot.get_channel( self.bot.guild_settings[guild.id]['shop_channel_id']) shop_message = await shop_channel.fetch_message( self.bot.guild_settings[guild.id]['shop_message_id']) if shop_message is None: self.logger.info( f"Can't update shop message - no message found (G{guild.id})" ) raise AttributeError() except (discord.NotFound, AttributeError): self.logger.info( f"Can't update shop message - no message/channel found (G{guild.id})" ) return # Generate embed coin_emoji = self.bot.guild_settings[guild.id].get("coin_emoji", None) or "coins" emojis = [] with utils.Embed() as embed: for emoji, data in self.get_shop_items(guild).items(): item_price = self.bot.guild_settings[guild.id].get( data['price_key'], data['amount']) if item_price <= 0: continue embed.add_field( f"{data['name']} ({emoji}) - {item_price} {coin_emoji}", data['description'], inline=False) emojis.append(emoji) # See if we need to edit the message try: current_embed = shop_message.embeds[0] except IndexError: current_embed = None if embed == current_embed: self.logger.info( f"Not updating shop message - no changes presented (G{guild.id})" ) return # Edit message self.logger.info(f"Updating shop message (G{guild.id})") await shop_message.edit(content=None, embed=embed) # Add reactions await shop_message.clear_reactions() for e in emojis: await shop_message.add_reaction(e)
async def cat(self, ctx:utils.Context): """Gives you some cats innit""" await ctx.channel.trigger_typing() headers = {"User-Agent": "MarriageBot/1.0.0 - Discord@Caleb#2831"} async with self.bot.session.get("https://api.thecatapi.com/v1/images/search", headers=headers) as r: data = await r.json() with utils.Embed(use_random_colour=True) as embed: embed.set_image(url=data[0]['url']) await ctx.send(embed=embed)
async def coins(self, ctx: utils.Context, user: discord.Member = None): """Gives you the content of your inventory""" # Make sure there's money set up if self.bot.guild_settings[ctx.guild.id].get( 'shop_message_id') is None: return await ctx.send( "There's no shop set up for this server, so there's no point in you getting money. Sorry :/" ) # Grab the user user = user or ctx.author if user.id == ctx.guild.me.id: return await ctx.send("Obviously, I'm rich beyond belief.") if user.bot: return await ctx.send( "Robots have no need for money as far as I'm aware.") # Get the data async with self.bot.database() as db: coin_rows = await db( "SELECT * FROM user_money WHERE guild_id=$1 AND user_id=$2", user.guild.id, user.id) if not coin_rows: coin_rows = [{'amount': 0}] inv_rows = await db( "SELECT * FROM user_inventory WHERE guild_id=$1 AND user_id=$2", user.guild.id, user.id) # Throw it into an embed coin_emoji = self.bot.guild_settings[ctx.guild.id].get( "coin_emoji", None) or "coins" with utils.Embed(use_random_colour=True) as embed: embed.set_author_to_user(user) inventory_string_rows = [] for row in inv_rows: if row['amount'] == 0: continue try: item_data = [ i for i in self.bot.get_cog( "ShopHandler").get_shop_items(ctx.guild).values() if i['name'] == row['item_name'] ][0] except IndexError: continue inventory_string_rows.append( f"{row['amount']}x {item_data['emoji'] or item_data['name']}" ) embed.description = f"**{coin_rows[0]['amount']:,} {coin_emoji}**\n" + "\n".join( inventory_string_rows) return await ctx.send(embed=embed)
def format_page(self, menu: menus.Menu, entries: list) -> utils.Embed: """Format the infraction entries into an embed""" with utils.Embed(use_random_colour=True) as embed: for row in entries: # TODO add timestamp embed.add_field( f"{row['infraction_type']} - {row['infraction_id']}", f"<@{row['moderator_id']}> :: {row['infraction_reason']}", inline=False) embed.set_footer( f"Page {menu.current_page + 1}/{self.get_max_pages()}") return embed
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="") # 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_message_edit(self, before: discord.Message, after: discord.Message): """Logs edited messages""" # Filter if after.guild is None: return if before.content == after.content: return if not before.content or not after.content: return if after.author.bot: return # Create embed with utils.Embed(colour=0x0000ff) as embed: embed.set_author_to_user(user=after.author) embed.description = f"[Message edited]({after.jump_url}) in {after.channel.mention}" before_content = before.content if len(before.content) > 1000: before_content = before.content[:1000] + '...' after_content = after.content if len(after.content) > 1000: after_content = after.content[:1000] + '...' embed.add_field(name="Old Message", value=before_content, inline=False) embed.add_field(name="New Message", value=after_content, inline=False) embed.set_footer(f"User ID {after.author.id}") embed.timestamp = after.edited_at if after.attachments: embed.add_field("Attachments", '\n'.join([i.url for i in after.attachments])) # Get channel channel_id = self.bot.guild_settings[after.guild.id].get( "edited_message_modlog_channel_id") channel = self.bot.get_channel(channel_id) if channel is None: return # Send log try: m = await channel.send(embed=embed) self.logger.info( f"Logging edited message (G{m.guild.id}/C{m.channel.id})") except discord.Forbidden: pass
async def fakesona(self, ctx: utils.Context): """Grabs you a fake sona from ThisFursonaDoesNotExist.com""" seed = random.randint(1, 99999) with utils.Embed(use_random_colour=True) as embed: embed.set_author( name="Click here to it larger", url= f"https://thisfursonadoesnotexist.com/v2/jpgs-2x/seed{seed:0>5}.jpg" ) embed.set_image( f"https://thisfursonadoesnotexist.com/v2/jpgs/seed{seed:0>5}.jpg" ) embed.set_footer(text="Provided by ThisFursonaDoesNotExist.com") await ctx.send(embed=embed)
async def interactions(self, ctx: utils.Context, user: discord.Member = None): """Shows you your interaction statistics""" if ctx.invoked_subcommand is not None: return # Get the interaction numbers user = user or ctx.author async with self.bot.database() as db: valid_interaction_rows = await db( "SELECT DISTINCT interaction_name FROM interaction_text WHERE guild_id=ANY($1::BIGINT[])", [0, ctx.guild.id]) given_rows = await db( "SELECT interaction, SUM(amount) AS amount FROM interaction_counter WHERE user_id=$1 AND guild_id=$2 GROUP BY guild_id, user_id, interaction", user.id, ctx.guild.id) received_rows = await db( "SELECT interaction, SUM(amount) AS amount FROM interaction_counter WHERE target_id=$1 AND guild_id=$2 GROUP BY guild_id, target_id, interaction", user.id, ctx.guild.id) valid_interactions = sorted( [i['interaction_name'] for i in valid_interaction_rows]) # Sort them into useful dicts given_interactions = collections.defaultdict(int) for row in given_rows: given_interactions[row['interaction']] = row['amount'] received_interactions = collections.defaultdict(int) for row in received_rows: received_interactions[row['interaction']] = row['amount'] # And now into an embed with utils.Embed(use_random_colour=True) as embed: embed.set_author_to_user(user=user) for i in valid_interactions: given = given_interactions[i] received = received_interactions[i] if given > 0 and received > 0: fraction = fractions.Fraction(given, received) embed.add_field( i.title(), f"Given {given}, received {received} ({fraction.numerator:.0f}:{fraction.denominator:.0f})" ) else: embed.add_field(i.title(), f"Given {given}, received {received}") await ctx.send(embed=embed)
async def stats(self, ctx: utils.Context): """Gives you the stats for the bot""" # Get creator info creator_id = self.bot.config["owners"][0] creator = self.bot.get_user(creator_id) or await self.bot.fetch_user( creator_id) # Make embed embed = utils.Embed(colour=0x1e90ff) embed.set_footer(str(self.bot.user), icon_url=self.bot.user.avatar_url) embed.add_field("Creator", f"{creator!s}\n{creator_id}") embed.add_field("Library", f"Discord.py {discord.__version__}") if self.bot.shard_count != len(self.bot.shard_ids): embed.add_field( "Approximate Guild Count", int((len(self.bot.guilds) / len(self.bot.shard_ids)) * self.bot.shard_count)) else: embed.add_field("Guild Count", len(self.bot.guilds)) embed.add_field("Shard Count", self.bot.shard_count) embed.add_field("Average WS Latency", f"{(self.bot.latency * 1000):.2f}ms") embed.add_field( "Coroutines", f"{len([i for i in asyncio.Task.all_tasks() if not i.done()])} running, {len(asyncio.Task.all_tasks())} total." ) if self.bot.config.get("topgg_token"): params = {"fields": "points,monthlyPoints"} headers = {"Authorization": self.bot.config['topgg_token']} async with self.bot.session.get( f"https://top.gg/api/bots/{self.bot.user.id}", params=params, headers=headers) as r: try: data = await r.json() except Exception: data = {} if "points" in data and "monthlyPoints" in data: embed.add_field( "Top.gg Points", f"{data['points']} ({data['monthlyPoints']} this month)") # Send it out wew let's go await ctx.send(embed=embed)
async def on_moderation_action(self, moderator: discord.Member, user: discord.User, reason: str, action: str): """Looks for moderator actions being done and logs them into the relevant channel""" # Save to database db_reason = None if reason == '<No reason provided>' else reason async with self.bot.database() as db: code = await self.get_unused_infraction_id(db) await db( """INSERT INTO infractions (infraction_id, guild_id, user_id, moderator_id, infraction_type, infraction_reason, timestamp) VALUES ($1, $2, $3, $4, $5, $6, $7)""", code, moderator.guild.id, user.id, moderator.id, action, db_reason, dt.utcnow(), ) # Get log channel log_channel_id = self.bot.guild_settings[moderator.guild.id].get( f"{action.lower()}_modlog_channel_id", None) log_channel = self.bot.get_channel(log_channel_id) if log_channel is None: return # Make info embed with utils.Embed() as embed: embed.title = action embed.add_field("Moderator", f"{moderator.mention} (`{moderator.id}`)") embed.add_field("User", f"<@{user.id}> (`{user.id}`)") if reason: embed.add_field("Reason", reason, inline=False) # Send to channel try: await log_channel.send(embed=embed) except discord.Forbidden: pass
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): """Log users joining or leaving VCs""" if before.channel == after.channel: return update_log_id = self.bot.guild_settings[ member.guild.id]['voice_update_modlog_channel_id'] if not update_log_id: return channel = self.bot.get_channel(update_log_id) if not channel: return try: if before.channel is None: text = f"{member.mention} joined the **{after.channel.name}** VC." elif after.channel is None: text = f"{member.mention} left the **{before.channel.name}** VC." else: text = f"{member.mention} moved from the **{before.channel.name}** VC to the **{after.channel.name}** VC." if channel.permissions_for(channel.guild.me).embed_links: data = { "embed": utils.Embed(use_random_colour=True, description=text) } else: data = { "content": text, "allowed_mentions": discord.AllowedMentions(users=False) } await channel.send(**data) self.logger.info( f"Logging updated VC user (G{member.guild.id}/U{member.id})") except discord.Forbidden: pass
def format_page(self, menu, entries): with utils.Embed(use_random_colour=True) as embed: embed.description = '\n'.join(entries) return embed
async def shop(self, ctx: utils.Context): """Shows you the available plants""" # Get the experience 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'] else: user_experience = 0 plant_limit = 1 available_item_count = 0 # Used to make sure we can continue the command embed = utils.Embed(use_random_colour=True, description="") # See what we wanna get to doing embed.description += f"What would you like to spend your experience to buy, {ctx.author.mention}? 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 plants to the embed plant_text = [] for plant in sorted(available_plants.values()): if plant.required_experience <= user_experience and len( plant_level_rows) < plant_limit: plant_text.append( f"{plant.display_name.capitalize()} - `{plant.required_experience} exp`" ) available_item_count += 1 else: plant_text.append( f"~~{plant.display_name.capitalize()} - `{plant.required_experience} exp`~~" ) 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) # Add items to the embed item_text = [] if user_experience >= self.get_points_for_plant_pot( plant_limit) and plant_limit < self.HARD_PLANT_CAP: item_text.append( f"Pot - `{self.get_points_for_plant_pot(plant_limit)} exp`") available_item_count += 1 else: item_text.append( f"~~Pot - `{self.get_points_for_plant_pot(plant_limit)} exp`~~" ) for item in self.bot.items.values(): if user_experience >= item.price: item_text.append( f"{item.display_name.capitalize()} - `{item.price} exp`") available_item_count += 1 else: item_text.append( f"~~{item.display_name.capitalize()} - `{item.price} exp`~~" ) embed.add_field("Available Items", '\n'.join(item_text), inline=True) # See if we should cancel if available_item_count == 0: embed.description += "\n**There is currently nothing available which you can purchase.**\n" return await ctx.send(embed=embed) # Wait for them to respond await ctx.send(embed=embed) try: plant_type_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 type {ctx.author.mention}.") given_response = plant_type_message.content.lower().replace(' ', '_') # See if they want a plant pot if given_response == "pot": if plant_limit >= self.HARD_PLANT_CAP: 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 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!") elif '\n' in plant_name: await ctx.send( "You can't have names with multiple lines in them! Please give another one instead!" ) else: break # Save that 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) VALUES ($1, $2, $3, 0, $4) 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 WHERE user_id=$1", ctx.author.id, plant_type.required_experience, ) await ctx.send(f"Planted your **{plant_type.display_name}** seeds!")
async def make_graph(self, ctx, users:typing.List[int], window_days:int, *, colours:dict=None, segments:int=None): """Makes the actual graph for the thing innit mate""" # Make sure there's people if not users: return await ctx.send("You can't make a graph of 0 users.") if len(users) > 10: return await ctx.send("There's more than 10 people in that graph - it would take too long for me to generate.") # Pick up colours if colours is None: colours = {} # This takes a lil bit so let's gooooooo await ctx.channel.trigger_typing() # Set up our most used vars original = window_days truncation = None if window_days > 365: window_days = 365 truncation = f"shortened from your original request of {original} days for going over the 365 day max" if window_days > (dt.utcnow() - ctx.guild.me.joined_at).days: window_days = (dt.utcnow() - ctx.guild.me.joined_at).days truncation = f"shortened from your original request of {original} days as I haven't been in the guild that long" # Make sure there's actually a day if window_days == 0: window_days = 1 # Go through each day and work out how many points it has points_per_week_base = [0 for _ in range(window_days)] # A list of the amount of points the user have in each given day (index) points_per_week = collections.defaultdict(points_per_week_base.copy) async with self.bot.database() as db: for user_id in users: message_rows = await db( """SELECT COUNT(timestamp) AS count, generate_series FROM user_messages, generate_series(1, $3) WHERE user_id=$1 AND guild_id=$2 AND timestamp > TIMEZONE('UTC', NOW()) - CAST(CONCAT($3, ' days') AS INTERVAL) + (INTERVAL '1 day' * generate_series) - INTERVAL '7 days' AND timestamp <= TIMEZONE('UTC', NOW()) - CAST(CONCAT($3, ' days') AS INTERVAL) + (INTERVAL '1 day' * generate_series) GROUP BY generate_series ORDER BY generate_series ASC""", user_id, ctx.guild.id, window_days, ) for row in message_rows: points_per_week[user_id][row['generate_series'] - 1] += row['count'] vc_rows = await db( """SELECT COUNT(timestamp) AS count, generate_series FROM user_vc_activity, generate_series(1, $3) WHERE user_id=$1 AND guild_id=$2 AND timestamp > TIMEZONE('UTC', NOW()) - CAST(CONCAT($3, ' days') AS INTERVAL) + (INTERVAL '1 day' * generate_series) - INTERVAL '7 days' AND timestamp <= TIMEZONE('UTC', NOW()) - CAST(CONCAT($3, ' days') AS INTERVAL) + (INTERVAL '1 day' * generate_series) GROUP BY generate_series ORDER BY generate_series ASC""", user_id, ctx.guild.id, window_days, ) for row in vc_rows: points_per_week[user_id][row['generate_series'] - 1] += row['count'] // 5 self.logger.info(points_per_week[user_id]) # Don't bother uploading if they've not got any data if sum([sum(user_points) for user_points in points_per_week.values()]) == 0: return await ctx.send("They've not sent any messages that I can graph.") # Get roles role_data = self.bot.guild_settings[ctx.guild.id]['role_gain'] role_object_data = sorted([(threshold, ctx.guild.get_role(role_id)) for role_id, threshold in role_data.items() if ctx.guild.get_role(role_id)], key=lambda x: x[0]) # Build our output graph fig = plt.figure() ax = fig.subplots() # Plot data for user, i in points_per_week.items(): if user in colours: colour = colours.get(user) else: colour = format(hex(random.randint(0, 0xffffff))[2:], "0>6") rgb_colour = tuple(int(colour[i:i + 2], 16) / 255 for i in (0, 2, 4)) ax.plot(list(range(window_days)), i, 'k-', label=str(self.bot.get_user(user)) or user, color=rgb_colour) fig.legend(loc="upper left") # Set size MINOR_AXIS_STOP = 50 if role_object_data: graph_height = max([role_object_data[-1][0] + MINOR_AXIS_STOP, math.ceil((max([max(i) for i in points_per_week.values()]) + 1) / MINOR_AXIS_STOP) * MINOR_AXIS_STOP]) else: graph_height = math.ceil((max([max(i) for i in points_per_week.values()]) + 1) / MINOR_AXIS_STOP) * MINOR_AXIS_STOP ax.axis([0, window_days, 0, graph_height]) # Fix axies ax.axis('off') ax.grid(True) # Add background colour for zorder, tier in zip(range(-100, -100 + (len(role_object_data) * 2), 2), role_object_data): plt.axhspan(tier[0], graph_height, facecolor=f"#{tier[1].colour.value or 0xffffff:0>6X}", zorder=zorder) plt.axhspan(tier[0], tier[0] + 1, facecolor="#000000", zorder=zorder + 1) # Tighten border fig.tight_layout() # Output to user baybeeee fig.savefig('activity.png', bbox_inches='tight', pad_inches=0) with utils.Embed() as embed: embed.set_image(url="attachment://activity.png") await ctx.send(f"Activity graph in a {window_days} day window{(' (' + truncation + ')') if truncation else ''}, showing average activity over each 7 day period.", embed=embed, file=discord.File("activity.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.PLANT_WATER_COOLDOWN) > 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.PLANT_WATER_COOLDOWN)) - 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 plant_nourishment = plant_level_row[0]['plant_nourishment'] gained_experience = 0 original_gained_experience = 0 multipliers = [] # List[Tuple[float, "reason"]] additional_text = [] # List[str] topgg_voted = False if 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.PLANT_WATER_COOLDOWN) <= timedelta(seconds=30): gained_experience = int(gained_experience * 1.5) multipliers.append((1.5, "You watered within 30 seconds of your plant's cooldown resetting")) # See if we want to give them the voter bonus if self.bot.config.get('topgg_token'): if await self.get_user_voted(ctx.author.id): gained_experience = int(gained_experience * 1.2) multipliers.append((1.2, f"You [voted for the bot](https://top.gg/bot/{self.bot.user.id}/vote) on Top.gg")) topgg_voted = True # Update db 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 plant_nourishment < 0: return await ctx.send("You sadly pour water into the dry soil of your silently wilting plant :c") # Send our SPECIAL outputs 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(plant_nourishment) > plant_data.get_nourishment_display_level(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) # Let's embed the thing, f**k it embed = None if ctx.guild is None or ctx.channel.permissions_for(ctx.guild.me).embed_links: embed = utils.Embed( use_random_colour=True, description=output_lines[0] ) if len(output_lines) > 1: embed.add_field( "Multipliers", "\n".join([i.strip('') for i in output_lines[1:]]), inline=False ) if self.bot.config.get('topgg_token') and topgg_voted is False: embed.set_footer(f"Get a 1.2x exp multiplier by voting on Top.gg! ({ctx.prefix}vote)") output_lines.clear() return await ctx.send("\n".join(output_lines), embed=embed)
async def displayall(self, ctx: utils.Context, user: typing.Optional[utils.converters.UserID]): """Show you all of your plants""" # 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", user.id) if not plant_rows: return await ctx.send(f"<@{user.id}> has no available plants.", allowed_mentions=discord.AllowedMentions( users=[ctx.author])) user_rows = await db( "SELECT * FROM user_settings WHERE user_id=$1", user.id) await ctx.trigger_typing() # Filter into variables images = [] for plant_row in plant_rows: if plant_row and user_rows: display_data = self.get_display_data(plant_row, user_rows[0]) elif plant_row: display_data = self.get_display_data(plant_row, None) elif user_rows: display_data = self.get_display_data(None, user_rows[0]) else: display_data = self.get_display_data(None, None) images.append(self.get_plant_image(**display_data)) # Work out our numbers max_height = max([i.size[1] for i in images]) total_width = sum([i.size[0] for i in images]) # Create the new image new_image = Image.new("RGBA", ( total_width, max_height, )) width_offset = 0 for index, image in enumerate(images): if random.randint(0, 1): image = ImageOps.mirror(image) new_image.paste(image, ( width_offset, max_height - image.size[1], ), image) width_offset += image.size[0] # And Discord it up image = self.crop_image_to_content( new_image.resize(( new_image.size[0] * 5, new_image.size[1] * 5, ), Image.NEAREST)) image_to_send = self.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") await ctx.send(embed=embed, file=file)
async def suggest(self, ctx: utils.Context, *, suggestion: str): """Send in a suggestion to the server""" if not suggestion: raise utils.errors.MissingRequiredArgumentString("suggestion") # Ask where the suggestion is for user = ctx.author try: m = await user.send( "Is this a _bot_ suggestion (0\N{COMBINING ENCLOSING KEYCAP}) or a _server_ suggestion (1\N{COMBINING ENCLOSING KEYCAP})?" ) except discord.Forbidden: return await ctx.send("I couldn't send you a DM.") await ctx.send("Sent you a DM!") await m.add_reaction("0\N{COMBINING ENCLOSING KEYCAP}") await m.add_reaction("1\N{COMBINING ENCLOSING KEYCAP}") # See what they're talkin about try: reaction, _ = await self.bot.wait_for( "reaction_add", check=lambda r, u: r.message.id == m.id and u.id == user.id, timeout=120) except asyncio.TimeoutError: return await user.send("Timed out asking about your suggestion.") # Generate the embed with utils.Embed(use_random_colour=True) as embed: embed.description = suggestion if ctx.message.attachments: embed.add_field( "Attachments", ", ".join([i.url for i in ctx.message.attachments])) embed.set_author_to_user(ctx.author) embed.timestamp = ctx.message.created_at embed.set_footer(f"User ID {ctx.author.id}") # See how they reacted - assume it's a server suggestion if it was an invalid emoji if str(reaction.emoji) == "0\N{COMBINING ENCLOSING KEYCAP}": suggestion_channel_id = self.bot.config['channels'][ 'suggestion_channel'] suggestion_channel = self.bot.get_channel(suggestion_channel_id) try: await suggestion_channel.send(embed=embed) except (discord.HTTPException, AttributeError) as e: return await user.send( f"Your suggestion could not be sent in to the development team - {e}" ) return await user.send( "Your suggestion has been successfully sent in to the bot development team." ) # Send the suggestion to the server if ctx.guild is None: return await user.send( "You can't run this command in DMs if you're trying to make a server suggestion." ) suggestion_channel_id = self.bot.guild_settings[ ctx.guild.id]['suggestion_channel_id'] if not suggestion_channel_id: return await user.send( f"**{ctx.guild.name}** hasn't set up a suggestion channel for me to send suggestions to." ) suggestion_channel = self.bot.get_channel(suggestion_channel_id) if not suggestion_channel: return await user.send( f"**{ctx.guild.name}** has set up an invalid suggestion channel for me to send suggestions to." ) try: embed.set_footer("Send in a suggestion with the 'suggest' command") await suggestion_channel.send(embed=embed) except (discord.HTTPException, AttributeError) as e: return await user.send( f"I couldn't send in your suggestion into {suggestion_channel.mention} - {e}" ) return await user.send( "Your suggestion has been successfully sent in to the server's suggestions channel." )
async def on_message_delete(self, message: discord.Message): """Logs edited messages""" # Filter if message.guild is None: return if message.author.bot: return # Create embed with utils.Embed(colour=0xff0000) as embed: embed.set_author_to_user(user=message.author) embed.description = f"Message deleted in {message.channel.mention}" if message.content: if len(message.content) > 1000: embed.add_field(name="Message", value=message.content[:1000] + '...', inline=False) else: embed.add_field(name="Message", value=message.content, inline=False) embed.set_footer(f"User ID {message.author.id}") embed.timestamp = dt.utcnow() if message.attachments: embed.add_field( "Attachments", '\n'.join([ f"[Attachment {index}]({i.url}) ([attachment {index} proxy]({i.proxy_url}))" for index, i in enumerate(message.attachments, start=1) ])) # See if we can get who it was deleted by delete_time = dt.utcnow() if message.guild.me.guild_permissions.view_audit_log: changed = False async for entry in message.guild.audit_logs( action=discord.AuditLogAction.message_delete, limit=1): if entry.extra.channel.id != message.channel.id: break if entry.target.id != message.author.id: break if entry.extra.count == 1 and delete_time > entry.created_at + timedelta( seconds=0.1): break # I want the entry to be within 0.1 seconds of the message deletion elif entry.extra.count > 1: last_delete_entry = self.last_audit_delete_entry_id.get( message.guild.id, ( 0, -1, )) if last_delete_entry[0] != entry.id: break # Last cached entry is DIFFERENT to this entry if last_delete_entry[1] == entry.extra.count: break # Unchanged from last cached log self.last_audit_delete_entry_id[message.guild.id] = ( entry.id, entry.extra.count) changed = True embed.description = f"Message deleted in {message.channel.mention} (deleted by {entry.user.mention})" if changed is False: embed.description = f"Message deleted in {message.channel.mention} (deleted by user or a bot)" # Get channel channel_id = self.bot.guild_settings[message.guild.id].get( "deleted_message_modlog_channel_id") channel = self.bot.get_channel(channel_id) if channel is None: return # Send log try: m = await channel.send(embed=embed) self.logger.info( f"Logging deleted message (G{m.guild.id}/C{m.channel.id})") except discord.Forbidden: pass
async def make_graph(self, ctx, users:typing.List[discord.Member], window_days:int, colours:dict=None): """Makes the actual graph for the thing innit mate""" # Make sure there's people if not users: return await ctx.send("You can't make a graph of 0 users.") if len(users) > 10: return await ctx.send("There's more than 10 people in that graph - it would take too long for me to generate.") # Pick up colours if colours is None: colours = {} # This takes a lil bit so let's gooooooo await ctx.channel.trigger_typing() # Set up our most used vars original = window_days truncation = None if window_days > 365: window_days = 365 truncation = f"shortened from your original request of {original} days for going over the 365 day max" if window_days > (dt.utcnow() - min([i.joined_at for i in users])).days: window_days = (dt.utcnow() - min([i.joined_at for i in users])).days truncation = f"shortened from your original request of {original} days as {'someone you pinged has not' if len(users) > 1 else 'they have not'} been in the guild that long" if window_days > (dt.utcnow() - ctx.guild.me.joined_at).days: window_days = (dt.utcnow() - ctx.guild.me.joined_at).days truncation = f"shortened from your original request of {original} days as I haven't been in the guild that long" # Make sure there's actually a day if window_days == 0: window_days = 1 # Check our window, see if we can make it a lil bigger for them # if window_days <= 1: # window = 'minutes', window_days * 24 * 60, 24 * 60 if window_days <= 10: window = 'hours', window_days * 24, 24 else: window = 'days', window_days, 1 # Go through each day and work out how many points it has points_per_week_base = [0] * window[1] # A list of the amount of points the user have in each given day (index) points_per_week = collections.defaultdict(points_per_week_base.copy) for user in users: for index in range(window[1]): between = (7 * window[2]) + window[1] - index - 1, window[1] - index - 1 points_per_week[user][index] = len(utils.CachedMessage.get_messages_between( user.id, ctx.guild.id, after={window[0]: between[0]}, before={window[0]: between[1]} )) # Don't bother uploading if they've not got any data if sum([sum(user_points) for user_points in points_per_week.values()]) == 0: return await ctx.send("They've not sent any messages that I can graph.") # Get roles async with self.bot.database() as db: role_data = await db("SELECT role_id, threshold FROM role_gain WHERE guild_id=$1", ctx.guild.id) role_object_data = sorted([(row['threshold'], ctx.guild.get_role(row['role_id'])) for row in role_data if ctx.guild.get_role(row['role_id'])], key=lambda x: x[0]) # Build our output graph fig = plt.figure() ax = fig.subplots() # Plot data for user, i in points_per_week.items(): if user.id in colours: colour = colours.get(user.id) else: colour = format(hex(random.randint(0, 0xffffff))[2:], "0>6") rgb_colour = tuple(int(colour[i:i + 2], 16) / 255 for i in (0, 2, 4)) ax.plot(list(range(window[1])), i, 'k-', label=(user.nick or user.name), color=rgb_colour) fig.legend(loc="upper left") # Set size MINOR_AXIS_STOP = 50 if role_object_data: graph_height = max([role_object_data[-1][0] + MINOR_AXIS_STOP, math.ceil((max([max(i) for i in points_per_week.values()]) + 1) / MINOR_AXIS_STOP) * MINOR_AXIS_STOP]) else: graph_height = math.ceil((max([max(i) for i in points_per_week.values()]) + 1) / MINOR_AXIS_STOP) * MINOR_AXIS_STOP ax.axis([0, window[1], 0, graph_height]) # Fix axies ax.axis('off') ax.grid(True) # Add background colour for zorder, tier in zip(range(-100, -100 + (len(role_object_data) * 2), 2), role_object_data): plt.axhspan(tier[0], graph_height, facecolor=f"#{tier[1].colour.value or 0xffffff:0>6X}", zorder=zorder) plt.axhspan(tier[0], tier[0] + 1, facecolor=f"#000000", zorder=zorder + 1) # Tighten border fig.tight_layout() # Output to user baybeeee fig.savefig('activity.png', bbox_inches='tight', pad_inches=0) with utils.Embed() as embed: embed.set_image(url="attachment://activity.png") await ctx.send(f"Activity graph in a {window_days} day window{(' (' + truncation + ')') if truncation else ''}, showing average activity over each 7 day period.", embed=embed, file=discord.File("activity.png"))