Example #1
0
    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)
Example #2
0
    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()
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
    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)
Example #6
0
    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']}>")
Example #7
0
    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,
        )
Example #8
0
    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)
Example #9
0
    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()
Example #10
0
    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)
Example #11
0
    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()
Example #12
0
    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!")
Example #13
0
    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!")
Example #14
0
    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}**."
        )
Example #15
0
    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!")
Example #16
0
    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)
Example #17
0
    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')}>.")
Example #18
0
    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()
Example #19
0
    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))