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 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 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 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 set_up_crafting_recipe(self, ctx: utils.Context, item_name: str): """ Talks the user through setting up an item crafting recipe. """ # See if stuff's already been set up async with self.bot.database() as db: rows = await db( "SELECT * FROM craftable_items WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, item_name) if rows: # See if they want to remove their current setup valid_reactions = [ "\N{HEAVY MULTIPLICATION X}", "\N{BLACK QUESTION MARK ORNAMENT}" ] acquire_method_setup = await ctx.send( f"You already have an acquire method set up for crafting via the `{ctx.clean_prefix}craftitem {item_name}` command. Would you like to remove this (\N{HEAVY MULTIPLICATION X}) or change how the crafting works (\N{BLACK QUESTION MARK ORNAMENT})?" ) for e in valid_reactions: await acquire_method_setup.add_reaction(e) try: reaction, _ = await self.bot.wait_for( "reaction_add", timeout=120.0, check=self.get_reaction_add_check(ctx, acquire_method_setup, valid_reactions)) emoji = str(reaction.emoji) except asyncio.TimeoutError: return await ctx.send( "Timed out setting up an item acquirement via crafting - please try again later." ) # See if they just wanna delete if emoji == "\N{HEAVY MULTIPLICATION X}": async with self.bot.database() as db: await db( "DELETE FROM craftable_items WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, item_name) await db( "DELETE FROM craftable_item_ingredients WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, item_name) return await ctx.send( f"Deleted the crafting recipe for `{item_name}` items.") async with self.bot.database() as db: await db( "DELETE FROM craftable_items WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, item_name) await db( "DELETE FROM craftable_item_ingredients WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, item_name) ingredient_list = [] # Ask what the item will be made from initially ingredient_bot_message = await ctx.send( "What item, and how many of that item, make up an ingredient of this crafting recipe (eg `5 cat`, `1 pizza slice`, `69 bee`, etc)?\n(Items are not checked until the end, so make sure you're spelling things correctly)" ) try: ingredient_user_message = await self.bot.wait_for( "message", timeout=120.0, check=lambda m: m.author.id == ctx.author.id and m.channel.id == ctx.channel.id and m.content, ) except asyncio.TimeoutError: return await ctx.send( "Timed out setting up an item acquirement via crafting - please try again later." ) # Parse ingredient amount_str, *ingredient_name = ingredient_user_message.content.split( ' ') if not amount_str.isdigit(): their_value = await commands.clean_content().convert( ctx, amount_str) return await ctx.send( f"I couldn't convert `{their_value}` into an integer - please try again later." ) ingredient_list.append((int(amount_str), ' '.join(ingredient_name))) # Ask about the rest of the ingredients while True: ingredient_bot_message = await ctx.send( "Is there another item that's part of this recipe (eg `5 cat`, `1 pizza slice`, `69 bee`, etc)? If not, just react (\N{HEAVY MULTIPLICATION X}) below." ) await ingredient_bot_message.add_reaction( "\N{HEAVY MULTIPLICATION X}") try: done, pending = await asyncio.wait( [ self.bot.wait_for( 'message', check=lambda m: m.author.id == ctx.author.id and m. channel.id == ctx.channel.id and m.content), self.bot.wait_for('reaction_add', check=self.get_reaction_add_check( ctx, ingredient_bot_message, ["\N{HEAVY MULTIPLICATION X}"])), ], return_when=asyncio.FIRST_COMPLETED, timeout=120.0) for future in pending: future.cancel() # we don't need these anymore except asyncio.TimeoutError: return await ctx.send( "Timed out setting up an item acquirement via crafting - please try again later." ) # Did they message or react? result = done.pop().result() if isinstance(result, discord.Message): ingredient_user_message = result else: break # Parse ingredient amount_str, *ingredient_name = ingredient_user_message.content.split( ' ') if not amount_str.isdigit(): their_value = await commands.clean_content().convert( ctx, amount_str) return await ctx.send( f"I couldn't convert `{their_value}` into an integer - please try again later." ) ingredient_list.append( (int(amount_str), ' '.join(ingredient_name))) # Ask how many of the item should be created await ctx.send( f"How many `{item_name}` should be created from this crafting recipe?" ) try: item_create_amount_message = await self.bot.wait_for( "message", timeout=120.0, check=lambda m: m.author.id == ctx.author.id and m.channel.id == ctx.channel.id and m.content) except asyncio.TimeoutError: return await ctx.send( "Timed out setting up an item acquirement via crafting - please try again later." ) try: item_create_amount = int(item_create_amount_message.content) except ValueError: their_value = await commands.clean_content().convert( ctx, item_create_amount_message.content) return await ctx.send( f"I couldn't convert `{their_value}` into an integer - please try again later." ) # Check that all the given items exist db = await self.bot.database.get_connection() all_items = await db("SELECT * FROM guild_items WHERE guild_id=$1", ctx.guild.id) invalid_items = [ i for i in ingredient_list if i[1] not in [o['item_name'] for o in all_items] ] if invalid_items: await db.disconnect() return await ctx.send( f"You gave some invalid items in your ingredients - {invalid_items!s} - please try again later." ) # Add them to the database async with ctx.typing(): await db( """INSERT INTO craftable_items (guild_id, item_name, amount_created) VALUES ($1, $2, $3)""", ctx.guild.id, item_name, item_create_amount) for amount, ingredient_name in ingredient_list: await db( """INSERT INTO craftable_item_ingredients (guild_id, item_name, ingredient_name, amount) VALUES ($1, $2, $3, $4)""", ctx.guild.id, item_name, ingredient_name, amount) # And respond await db.disconnect() return await ctx.send("Your crafting recipe has been added!")
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 craftitem(self, ctx: utils.Context, *, crafted_item_name: commands.clean_content): """ Crafts a new item from your current inventory. """ # See if there's a crafting recipe set up crafted_item_name = crafted_item_name.lower() async with self.bot.database() as db: item_craft_amount = await db( "SELECT * FROM craftable_items WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, crafted_item_name) if not item_craft_amount: return await ctx.send( f"You can't acquire **{crafted_item_name}** items via the crafting." ) item_craft_ingredients = await db( "SELECT * FROM craftable_item_ingredients WHERE guild_id=$1 AND item_name=$2", ctx.guild.id, crafted_item_name) user_inventory = await db( "SELECT * FROM user_inventories WHERE guild_id=$1 AND user_id=$2", ctx.guild.id, ctx.author.id) # Add in some dictionaries to make this a lil easier ingredients = { i['ingredient_name']: i['amount'] for i in item_craft_ingredients } inventory_original = { i['item_name']: i['amount'] for i in user_inventory if i['item_name'] in ingredients } inventory = inventory_original.copy() # See if they have enough of the items max_craftable_amount = [] for ingredient, required_amount in ingredients.items(): if inventory.get(ingredient, 0) - required_amount < 0: return await ctx.send( f"You don't have enough **{ingredient}** items to craft this." ) max_craftable_amount.append( inventory.get(ingredient) // required_amount) max_craftable_amount = min(max_craftable_amount) # Make sure they wanna make it ingredient_string = [f"`{o}x {i}`" for i, o in ingredients.items()] await ctx.send( f"This craft gives you **{item_craft_amount[0]['amount_created']}x {crafted_item_name}** and is made from {', '.join(ingredient_string)}. You can make this between 0 and {max_craftable_amount} times - how many times would you like to craft this?" ) try: crafting_amount_message = await self.bot.wait_for( "message", timeout=120.0, check=lambda m: m.channel.id == ctx.channel.id and m.author.id == ctx.author.id and m.content) except asyncio.TimeoutError: return await ctx.send( "Timed out on crafting confirmation - please try again later.") # Get how many they want to craft, and make sure they can do it try: user_craft_amount = int(crafting_amount_message.content) except ValueError: their_value = await commands.clean_content().convert( ctx, crafting_amount_message.content) return await ctx.send( f"I couldn't convert `{their_value}` into an integer - please try again later." ) # See if they said 0 if user_craft_amount <= 0: return await ctx.send("Alright, aborting crafting!") # Remove the right amounts from their inventory for ingredient, required_amount in ingredients.items(): if inventory[ingredient] - (required_amount * user_craft_amount) < 0: return await ctx.send( f"You don't have enough **{ingredient}** items to craft this." ) inventory[ingredient] -= (required_amount * user_craft_amount) # Alter their inventory babey lets GO async with ctx.typing(): async with self.bot.database() as db: for item, amount in inventory.items(): await db( "UPDATE user_inventories SET amount=$4 WHERE guild_id=$1 AND user_id=$2 AND item_name=$3", ctx.guild.id, ctx.author.id, item, amount) await db( """INSERT INTO user_inventories (guild_id, user_id, item_name, amount) VALUES ($1, $2, $3, $4) ON CONFLICT (guild_id, user_id, item_name) DO UPDATE SET amount=user_inventories.amount+excluded.amount""", ctx.guild.id, ctx.author.id, crafted_item_name, item_craft_amount[0]['amount_created'] * user_craft_amount) return await ctx.send( f"You've sucessfully crafted **{item_craft_amount[0]['amount_created'] * user_craft_amount:,}x {crafted_item_name}**." )
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." ) # 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, ]) # 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.", ignore_error=True) 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.", ignore_error=True) 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 how much debt the user can go into # await ctx.send("""How much debt do you want users to be able to go into with this currency? Use "0" for no debt, or a number for any amount.""") # for _ in range(3): # try: # currency_debt_message = await self.bot.wait_for("message", check=check, timeout=60) # assert currency_debt_message.content # int(currency_debt_message.content) # assert int(currency_debt_message.content) >= 0 # break # except asyncio.TimeoutError: # return await ctx.send("Timed out on adding a new currency to the guild.", ignore_error=True) # except (AssertionError, ValueError): # await currency_debt_message.reply("This isn't a valid number - please provide another one.") # else: # return await ctx.send("You failed giving a valid currency debt amount too many times - please try again later.") # Ask if we should add a daily command 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 _ in range(3): try: currency_debt_message = await self.bot.wait_for("message", check=check, timeout=60) assert currency_debt_message.content int(currency_debt_message.content) assert int(currency_debt_message.content) >= 0 break except asyncio.TimeoutError: return await ctx.send( "Timed out on adding a new currency to the guild.", ignore_error=True) except (AssertionError, ValueError): await currency_debt_message.reply( "This isn't a valid number - please provide another one.") else: return await ctx.send( "You failed giving a valid currency debt amount too many times - please try again later." ) # 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, negative_amount_allowed) VALUES ($1, $2, $3, $4)""", ctx.guild.id, currency_name_message.content, currency_short_message.content, 0, ) return await ctx.send("Added a new currency to your server!")
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 issue_create(self, ctx:utils.Context, repo:GitRepo, *, title:str): """ Create a Github issue on a given repo. """ # 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`.") # Ask if we want to do this embed = utils.Embed(title=title, use_random_colour=True).set_footer("Use the \N{HEAVY PLUS SIGN} emoji to add a body.") m = await ctx.send("Are you sure you want to create this issue?", embed=embed) valid_emojis = ["\N{THUMBS UP SIGN}", "\N{HEAVY PLUS SIGN}", "\N{THUMBS DOWN SIGN}"] body = None while True: if body: embed = utils.Embed( title=title, description=body, use_random_colour=True ).set_footer("Use the \N{HEAVY PLUS SIGN} emoji to change the body.") else: for e in valid_emojis: self.bot.loop.create_task(m.add_reaction(e)) try: check = lambda p: p.message_id == m.id and str(p.emoji) in valid_emojis and p.user_id == ctx.author.id payload = await self.bot.wait_for("raw_reaction_add", check=check, timeout=120) except asyncio.TimeoutError: return await ctx.send("Timed out asking for issue create confirmation.") # Get the body if str(payload.emoji) == "\N{HEAVY PLUS SIGN}": n = await ctx.send("What body content do you want to be added to your issue?") try: check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id body_message = await self.bot.wait_for("message", check=check, timeout=60 * 5) except asyncio.TimeoutError: return await ctx.send("Timed out asking for issue body text.") attachment_urls = [] for i in body_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 try: await n.delete() await body_message.delete() except discord.HTTPException: pass try: await m.remove_reaction("\N{HEAVY PLUS SIGN}", ctx.author) except discord.HTTPException: pass body = body_message.content + "\n\n" for name, url in attachment_urls: body += f"![{name}]({url})\n" embed = utils.Embed(title=title, description=body, use_random_colour=True) await m.edit(contnet="Are you sure you want to create this issue?", embed=embed) # Check the reaction if str(payload.emoji) == "\N{THUMBS DOWN SIGN}": return await ctx.send("Alright, cancelling issue add.") if str(payload.emoji) == "\N{THUMBS UP SIGN}": break # Work out our args if repo.host == "Github": json = {'title': title, 'body': body.strip()} headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f"token {user_rows[0]['github_access_token']}"} elif repo.host == "Gitlab": json = {'title': title, 'description': body.strip()} headers = {'Authorization': f"Bearer {user_rows[0]['gitlab_bearer_token']}"} headers.update({'User-Agent': self.bot.user_agent}) # Make the post request async with self.bot.session.post(repo.issue_api_url, json=json, headers=headers) as r: data = await r.json() self.logger.info(f"Received data from git {r.url!s} - {data!s}") if 200 <= r.status < 300: pass else: return await ctx.send(f"I was unable to create an issue on that repository - `{data}`.") await ctx.send(f"Your issue has been created - <{data.get('html_url') or data.get('web_url')}>.")
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 treemaker(self, ctx: vbu.Context, user_id: int, stupid_tree: bool = False): """ Handles the generation and sending of the tree to the user. """ # Get their family tree user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) # Make sure they have one if user_info.is_empty: if user_id == ctx.author.id: return await ctx.send( "You have no family to put into a tree .-.") return await ctx.send( f"**{utils.escape_markdown(user_name)}** has no family to put into a tree .-.", allowed_mentions=discord.AllowedMentions.none(), ) # Get their customisations async with self.bot.database() as db: ctu = await utils.CustomisedTreeUser.fetch_by_id(db, ctx.author.id) # Get their dot script async with ctx.typing(): if stupid_tree: dot_code = await user_info.to_full_dot_script(self.bot, ctu) else: dot_code = await user_info.to_dot_script(self.bot, ctu) # Write the dot to a file dot_filename = f'{self.bot.config["tree_file_location"]}/{ctx.author.id}.gz' try: with open(dot_filename, 'w', encoding='utf-8') as a: a.write(dot_code) except Exception as e: self.logger.error(f"Could not write to {dot_filename}") raise e # Convert to an image image_filename = f'{self.bot.config["tree_file_location"].rstrip("/")}/{ctx.author.id}.png' # http://www.graphviz.org/doc/info/output.html#d:png perks = await utils.get_marriagebot_perks(ctx.bot, ctx.author.id) # highest quality colour, and antialiasing # not using this because not much point # todo: add extra level for better colour, stroke etc, basically like the one in the readme (in addition to antialiasing) # if False: # format_rendering_option = '-Tpng:cairo' # -T:png does the same thing but this is clearer # normal colour, and antialising if perks.tree_render_quality >= 1: format_rendering_option = '-Tpng:cairo' # normal colour, no antialising else: format_rendering_option = '-Tpng:gd' dot = await asyncio.create_subprocess_exec('dot', format_rendering_option, dot_filename, '-o', image_filename, '-Gcharset=UTF-8') await asyncio.wait_for(dot.wait(), 10.0, loop=self.bot.loop) # Kill subprocess try: dot.kill() except ProcessLookupError: pass # It already died except Exception: raise # Send file try: file = discord.File(image_filename) except FileNotFoundError: return await ctx.send( "I was unable to send your family tree image - please try again later." ) text = "[Click here](https://marriagebot.xyz/) to customise your tree." if not stupid_tree: text += f" Use `{ctx.prefix}bloodtree` for your _entire_ family, including non-blood relatives." tree_message = await ctx.send(text, file=file) await self.bot.add_delete_reaction(tree_message) # Delete the files self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', dot_filename)) self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', image_filename))