示例#1
0
class Template(Cog):
    def __init__(self, bot: TLDR):
        self.bot = bot

    @command(
        help='General description of the command/what the command does',
        usage='template_command [arg that is required] (arg that is optional)',
        examples=['template_command needed_arg', 'template_command needed_arg2 optional_arg'],
        Admins=commands.Help(
            # clearance in here will be set to Admin automatically, it doesnt need to be defined
            help='You can specify different help for people with higher perms, specially defined help like this one needs to be a higher clearance than the base one',
            usage='template_command [args]',
            examples=['template_command --arg1'],
            command_args=[
                # the third value in the arg tuple is the type the arg value will be converted to, if the type is a list, multiple of the same arg will be pushed to a list
                # the type can also be a command, like modules.format_time.parse
                # If the shorter arg is set to None, the user will only be presented with the long option
                (('--arg1', '-a1', str), 'Description of arg1'),  # These need to be defined when using ParseArgs, otherwise it won't know what to look for
                (('--arg2', None, list), 'Description of arg2')
            ]
        ),
        cls=commands.Command  # here so we can actually use all the custom kwargs\
    )
    # when giving an arg a type, discord.py will attempt to convert that arg to the given type
    # rest_of_the_args will attempt to convert to ParseArgs, if it fails, it'll convert to str
    def template_command(self, ctx: Context, first_arg: str = None, *, rest_of_the_args: Union[ParseArgs, str] = None):
        # if the command doesnt do anything without args, it's best to include this line in the beginning
        # it'll send info about the command and how to use it
        if first_arg is None:
            return await embed_maker.command_error(ctx)

        # if you want to send a response, please use embed_maker.message or embed_maker.error
        # with embed_maker.message you will need to give it `send` kwarg, if send is missing, it'll return an embed
        if first_arg.isdigit():
            return await embed_maker.error(ctx, 'Error message')
        else:
            return await embed_maker.message(ctx, description='Description or whatever', send=True)

    @commands.group(
        invoke_without_command=True, # this kwarg needs to be here, otherwise subcommands will be ignored
        help='General description of the command/what the command does',
        usage='template_group_command [required arg]',
        examples=['template_command needed_arg 123'],
        cls=commands.Group  # don't forget to change this to cls.Group
    )
    async def template_group_command(self, ctx: Context, *, args: str):
        pass

    @template_group_command.command(
        name='sub_command', # the name needs to be defined on sub commands
        help='Description',
        usage='template_group_command sub_command [args]',
        examples=['template_group_command sub_command hello 123'],
        cls=commands.Command,
    )
    async def template_group_command_sub_command(self, ctx: Context, *, args: str):
        pass
示例#2
0
class UK(commands.Cog):
    def __init__(self, bot: TLDR):
        self.bot = bot
        self.loaded = False

    def load(self):
        self.ukparl_module = self.bot.ukparl_module
        self.parliament = self.ukparl_module.parliament
        self.loaded = True

    @staticmethod
    async def construct_bills_search_embed(
        ctx: Context,
        bills: list[Bill],
        max_page_num: int,
        page_limit: int,
        *,
        page: int,
    ):
        if len(bills) == 0:
            return await embed_maker.message(ctx, description="No bills found.")

        bits = []
        next_line = "\n"
        for i, bill in enumerate(bills[page_limit * (page - 1) : page_limit * page]):
            bill_title = bill.get_title()
            description = None
            if bill.get_long_title() is not None:
                description = bill.get_long_title()[0:160] + "..."

            bill_id = bill.get_bill_id()
            bill_url = f"https://bills.parliament.uk/bills/{bill.get_bill_id()}"
            entry = f"**{(i + 1) + (page_limit * (page - 1))}. [{bill_title}]({bill_url}) | ID: {bill_id}**{next_line}"
            if description is not None:
                entry = entry + f"**Description:** {description}"
            bits.append(entry)

        embed = await embed_maker.message(
            ctx,
            description=next_line.join(bits),
            author={"name": "UKParliament Bills"},
            footer={"text": f"Page {page}/{max_page_num}"},
        )
        return embed

    @staticmethod
    async def construct_bill_info_embed(
        ctx: Context,
        bill: Bill,
        c_divisions: list[CommonsDivision],
        l_divisions: list[LordsDivision],
        page_limit: int,
        *,
        page: int,
    ):
        formatted_bill_information = [
            f"**Title:** {bill.get_title()}",
            f"**Description:** {bill.get_long_title()}",
            # f"**Introduced:** {bill.get_date_introduced().strftime('%Y-%m-%d %H:%M:%S')}",
            f"**Last Update:** {bill.get_last_update().strftime('%Y-%m-%d %H:%M:%S')}",
            f"**Act of Parliament:** {'Yes' if bill.is_act() else 'No'}",
        ]

        if bill.has_royal_assent():
            formatted_bill_information.append(
                "**Current Stage:** Received Royal Assent"
            )
        else:
            formatted_bill_information.extend(
                [
                    f"**Current Stage:** {bill.get_current_stage().get_name()}",
                    f"**Current House:** {bill.get_current_house()}",
                ]
            )

        total_length = (
            len(l_divisions) + len(c_divisions) + len(formatted_bill_information)
        )
        max_pages = math.ceil(total_length / page_limit)
        pages: list[embeds.Embed] = []

        async def template(description: str, title: str) -> embeds.Embed:
            return await embed_maker.message(
                ctx,
                title=title,
                description=description,
                author={"name": "UKParliament Bill"},
                footer={"text": f"Page: {page}/{max_pages}"},
            )  # type: ignore

        next_line = "\n"
        if len(l_divisions) > 0:
            bits = []
            for i, division in enumerate(l_divisions):
                did_pass = division.get_aye_count() > division.get_no_count()
                bits.append(
                    f"**{i + 1}. [{division.get_division_title()}](https://votes.parliament.uk/Votes/"
                    f"Lords/Division/{division.get_id()})**{next_line}"
                    f"**ID:** {division.get_id()}{next_line}"
                    f"**Summary:** "
                    f"{division.get_amendment_motion_notes()[0:150].replace('<p>', '').replace('</p>', '')}..."
                    if division.get_amendment_motion_notes() is not None
                    and division.get_amendment_motion_notes() != ""
                    else ""
                    f"{next_line}**Division Result:** {'Passed' if did_pass else 'Not passed'} by a division"
                    f"of {division.get_aye_count() if did_pass else division.get_no_count()}"
                    f"{'Ayes' if did_pass else 'Noes'} to "
                    f"{division.get_aye_count() if did_pass is False else division.get_no_count()}"
                    f" {'Noes' if did_pass else 'Ayes'}"
                    f"{next_line}**Division Date:** {division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}"
                )

                if i == (page_limit - 1) or i == (len(l_divisions) - 1):
                    pages.append(
                        await template(
                            title="Lords Divisions", description="\n".join(bits)
                        )
                    )
                    bits.clear()

        if len(c_divisions) > 0:
            bits = []
            for i, division in enumerate(c_divisions):
                did_pass = division.get_aye_count() > division.get_no_count()
                bits.append(
                    f"**{i + 1}. [{division.get_division_title()}](https://votes.parliament.uk/"
                    f"Votes/Lords/Division/{division.get_id()})**{next_line}"
                    f"**ID:** {division.get_id()}{next_line}**Division Result:**"
                    f"{'Passed' if did_pass else 'Not passed'} by a division of"
                    f" {division.get_aye_count() if did_pass else division.get_no_count()}"
                    f"{'Ayes' if did_pass else 'Noes'} to "
                    f"{division.get_aye_count() if did_pass is False else division.get_no_count()}"
                    f"{'Noes' if did_pass else 'Ayes'}{next_line}**Division Date:**"
                    f"{division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}"
                )
                if i == (page_limit - 1) or i == (len(l_divisions) - 1):
                    pages.append(
                        await template(
                            title="Commons Divisions", description="\n".join(bits)
                        )
                    )
                    bits.clear()

        first_page: embeds.Embed = await embed_maker.message(
            ctx,
            description="\n".join(formatted_bill_information),
            author={"name": "UKParliament Bill"},
            footer={"text": f"Page: {page}/{max_pages}"},
        )  # type: ignore
        pages.insert(0, first_page)
        return (pages[(page - 1)], len(pages))

    @staticmethod
    async def construct_divisions_lords_embed(
        ctx: commands.Context,
        divisions: list[LordsDivision],
        page_limit: int,
        *,
        page: int,
    ):
        max_pages = math.ceil(len(divisions) / page_limit)

        bits = []
        next_line = "\n"
        for i, division in enumerate(
            divisions[page_limit * (page - 1) : page_limit * page]
        ):
            did_pass = division.get_aye_count() > division.get_no_count()
            bits.append(
                f"**{(page_limit * (page -1)) + i + 1}. [{division.get_division_title()}]"
                f"(https://votes.parliament.uk/Votes/Lords/Division/{division.get_id()})**{next_line}"
                f"**ID:** {division.get_id()}{next_line}**Summary:** {division.get_amendment_motion_notes()[0:150]}..."
                f"{next_line}**Division Result:** {'Passed' if did_pass else 'Not passed'} by a division of "
                f"{division.get_aye_count() if did_pass else division.get_no_count()} {'Ayes' if did_pass else 'Notes'}"
                f" to {division.get_no_count() if did_pass else division.get_aye_count()} "
                f"{'Noes' if did_pass else 'Ayes'}{next_line}**Division Date:** "
                f"{division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}"
            )

        embed = await embed_maker.message(
            ctx,
            description=next_line.join(bits),
            author={"name": "UKParliament Division"},
            footer={"text": f"Page  {page}/{max_pages}"},
        )
        return (embed, max_pages)

    @staticmethod
    async def construct_divisions_commons_embed(
        ctx: commands.Context,
        divisions: list[CommonsDivision],
        page_limit: int,
        *,
        page: int,
    ):
        max_pages = math.ceil(len(divisions) / page_limit)
        next_line = "\n"
        bits = []
        for i, division in enumerate(
            divisions[page_limit * (page - 1) : page_limit * page]
        ):
            did_pass = division.get_aye_count() > division.get_no_count()
            bits.append(
                f"**{(page_limit * (page -1)) + i + 1}. [{division.get_division_title()[0:100]}]"
                f"(https://votes.parliament.uk/Votes/Commons/Division/{division.get_id()})**{next_line}"
                f"**ID:** {division.get_id()}{next_line}**Division Result:** {'Passed' if did_pass else 'Not passed'}"
                f" by a division of {division.get_aye_count() if did_pass else division.get_no_count()} "
                f"{'Ayes' if did_pass else 'Notes'} to "
                f"{division.get_no_count() if did_pass else division.get_aye_count()} {'Noes' if did_pass else 'Ayes'}"
                f"{next_line}**Division Date:** {division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}"
            )

        embed: embeds.Embed = await embed_maker.message(
            ctx,
            description=next_line.join(bits),
            author={"name": "UKParliament Division"},
            footer={"text": f"Page {page}/{max_pages}"},
        )  # type: ignore
        return embed, max_pages

    @commands.group(
        help="To access the commands interfacing the UK Parliament Site.",
        invoke_without_command=True,
        usage="uk [sub command]",
        examples=["uk divisions linfo 1234", "uk mpelection Boris Johnson"],
        Mod=cls.Help(
            help="To access the commands inferfacing with the UK Parliament Site. And to access commands relevant to"
            " the configuration of this feature",
            usage="uk [sub command]",
            examples=["uk mod tracker channels"],
            sub_commands=["bills", "divisions", "minfo", "mpelection", "mod"],
        ),
        sub_commands=[
            "bills",
            "divisions",
            "minfo",
            "mpelection",
        ],
        cls=cls.Group,
    )
    async def uk(self, ctx: commands.Context):
        return await embed_maker.command_error(ctx)

    @uk.group(
        help="For commands relating to bills",
        invoke_without_command=True,
        usage="uk bills [sub command]",
        examples=["uk bills search European Withdrawal"],
        sub_commands=["search"],
        cls=cls.Group,
    )
    async def bills(self, ctx: commands.Context):
        return await embed_maker.command_error(ctx)

    @uk.group(
        name="divisions",
        help="For commands relating to divisions",
        invoke_without_command=True,
        usage="uk divisions [sub command]",
        examples=["uk divisions lsearch [args]"],
        sub_commands=["lsearch", "csearch", "linfo", "cinfo"],
        cls=cls.Group,
    )
    async def divisions(self, ctx: commands.Context):
        return await embed_maker.command_error(ctx)

    @uk.group(
        name="mod",
        invoke_without_command=True,
        help="Moderator level commands for this feature",
        usage="uk mod [sub command]",
        examples=["uk mod tracker [args]"],
        sub_commands=["tracker"],
        cls=cls.Group,
    )
    async def mod_commands(self, ctx: commands.Context):
        return await embed_maker.command_error(ctx)

    @mod_commands.group(
        name="tracker",
        invoke_without_command=True,
        help="Commands related to the trackering section of this feature",
        usage="uk mod tracker [sub command]",
        examples=["uk mod tracker channels"],
        sub_commands=[
            "channels",
            # "load",
            "statuses",
            "loop",
            "dbclear",
            "dbstats",
            "ping",
        ],
        cls=cls.Group,
    )
    async def mod_cmd_tracker(self, ctx: commands.Context):
        return await embed_maker.command_error(ctx)

    @mod_commands.command(
        name="ping",
        help="Pings an endpoint on the REST api and returns the latency between the bot and a REST request.",
        usage="uk mod ping",
        examples=["uk mod ping"],
        cls=cls.Command,
    )
    async def mod_cmd_ping(self, ctx: commands.Context):
        if self.loaded is False:
            return

        start = datetime.now()

        async with self.ukparl_module.get_aiohttp_session().get(
            f"{URL_BILLS}/BillTypes"
        ) as resp:
            if resp.status != 200:
                return await embed_maker.message(
                    ctx=ctx,
                    description=f"Couldn't get ping, status: {resp.status}",
                    send=True,
                )
            end = datetime.now()
            difference = end - start
            await embed_maker.message(
                ctx=ctx, description=f"{difference.microseconds / 1000} ms", send=True
            )

    @mod_cmd_tracker.command(
        name="dbclear",
        help=(
            "Clear all recorded entries from the two MongoDB collectioned used by the tracker."
            "Doing this will produce from the bot duplicate tracker announcement from the last few days."
        ),
        usage="uk mod tracker dbclear",
        examples=["uk mod tracker dbclear", "uk mod tracker dbclear [auth code]"],
        cls=cls.Command,
    )
    async def mod_cmd_tracker_db_clear(self, ctx: commands.Context, code: str = ""):
        if self.loaded is False:
            return

        confirm_manager = self.ukparl_module.confirm_manager

        if confirm_manager.has_code(ctx.author):
            if code == "":
                return await embed_maker.message(
                    ctx,
                    description="Confirm code required to execute this command successfully.",
                    send=True,
                )
            if confirm_manager.confirm_code(ctx.author, code):
                database.get_connection().clear_bills_tracker_collection()
                database.get_connection().clear_divisions_tracker_collection()
                return await embed_maker.message(
                    ctx,
                    description="Cleared bills_tracker and division_tracker collections.",
                    send=True,
                )
            else:
                return await embed_maker.message(
                    ctx, description="Confirm code incorrect.", send=True
                )
        code = confirm_manager.gen_code(ctx.author)
        await embed_maker.message(
            ctx,
            description="Sending confirmation to delete contents of 'bills_tracker' and 'divisions_tracker'.",
            send=True,
        )
        await ctx.author.send(f"Code to confirm clear Mongo Collections: {code}")

    @mod_cmd_tracker.command(
        name="dbstats",
        help="At the moment this only displays the amount of documents in both bills_tracker and divisions_tracker collections",
        usage="uk mod tracker dbstats",
        clearence="dev",
        cls=cls.Command,
    )
    async def mod_cmd_tracker_db_stats(self, ctx: commands.Context):
        if self.loaded is False:
            return
        next_line = "\n"
        await embed_maker.message(
            ctx,
            description=(
                f"**Collection Count**{next_line}- bills_tracker:"
                f" {database.get_connection().get_bills_tracker_count()}{next_line}- divisions_tracker:"
                f" {database.get_connection().get_divisions_tracker_count()}"
            ),
            send=True,
        )

    @mod_cmd_tracker.command(
        name="statuses",
        help="Show the status of each tracker",
        usage="uk mod tracker statuses",
        examples=["uk mod tracker statuses"],
        cls=cls.Command,
    )
    async def mod_cmd_tracker_statuses(self, ctx: commands.Context):
        if self.loaded is False:
            return  # type: ignore
        next_line = "\n"
        statuses = self.bot.ukparl_module.tracker_status
        bits = []

        for key in statuses.keys():
            entry = statuses[key]
            if key == "loop":
                bits.append(
                    f"**Loop Status**: {'Enabled' if entry is True else 'Disabled'}"
                )
                continue

            bits.append(
                f"{next_line}**{key}**:{next_line}  - **Started:** {'Yes' if entry['started'] else 'No'}{next_line}  - "
                f"**Confirmed:** {'Yes' if entry['confirmed'] else 'No'}"
            )

        return await embed_maker.message(
            ctx,
            description=f"**Status for each Tracker**{next_line}{next_line.join(bits)}",
            send=True,
        )

    @mod_cmd_tracker.group(
        name="channels",
        invoke_without_command=True,
        usage="uk mod tracker channels [args]",
        examples=["uk mod tracker channels", "uk mod tracker channels set [args]"],
        cls=cls.Group,
    )
    async def mod_cmd_tracker_channels(self, ctx: commands.Context):
        if self.loaded is False:
            return  # type: ignore
        bits = []

        channels = self.ukparl_module.config.get_channel_ids()

        for key in channels.keys():
            channel_id = channels[key]
            guild = self.ukparl_module.get_guild()
            if guild is not None:
                channel = guild.get_channel(int(channel_id))
                bits.append(
                    f"- {key}:" f" Couldn't find channel ({channel_id})"
                    if channel is None
                    else f"- {key}: {channel.name}"
                )
            else:
                bits.append("Couldn't fetch guild.")

        next_line = "\n"
        await embed_maker.message(
            ctx, description=f"**Channels**{next_line}{next_line.join(bits)}", send=True
        )

    @mod_cmd_tracker.group(
        name="loop",
        invoke_without_command=True,
        help="Commands to start and stop the event loop checking the various rss feeds",
        usage="uk mod tracker loop",
        examples=["uk mod tracker loop start", "uk mod tracker loop stop"],
        cls=cls.Group,
    )
    async def mod_cmd_tracker_eventloop(self, ctx: commands.Context):
        if self.loaded is False:
            return
        return await embed_maker.message(
            ctx,
            description=(
                "The event loop is currently "
                f"{'running' if self.bot.ukparl_module.tracker_event_loop.is_running() else 'not running'}."
            ),
            send=True,
        )

    @mod_cmd_tracker_eventloop.command(
        name="start",
        help="Start the event loop.",
        usage="uk mod tracker loop start",
        examples=["uk mod tracker loop start"],
        cls=cls.Command,
    )
    async def mod_cmd_tracker_eventloop_start(self, ctx: commands.Context):
        if self.loaded is False:
            return  # type: ignore
        tracker_statuses = self.bot.ukparl_module.tracker_status
        config = self.bot.ukparl_module.config

        if (
            config.get_channel_id("feed") == 0
            and tracker_statuses["feed"]["started"] is True
        ):
            return await embed_maker.message(
                ctx,
                description="A listener registered to the 'feed' tracker doesn't have a channel to output to.",
                send=True,
            )

        if self.bot.ukparl_module.tracker_event_loop.is_running() is False:
            self.bot.ukparl_module.tracker_event_loop.start()
            self.bot.ukparl_module.tracker_status["loop"] = True
            await embed_maker.message(ctx, description="Started event loop.", send=True)
        else:
            await embed_maker.message(
                ctx, description="Event loop is already running.", send=True
            )

    @mod_cmd_tracker_eventloop.command(
        name="stop",
        help="Stop the event loop",
        usage="uk mod tracker loop stop",
        examples=["uk mod tracker loop stop"],
        cls=cls.Command,
    )
    async def mod_cmd_tracker_eventloop_stop(self, ctx: commands.Context):
        if self.loaded is False:
            return  # type: ignore
        if self.bot.ukparl_module.tracker_event_loop.is_running() is True:
            self.bot.ukparl_module.tracker_event_loop.stop()
            await embed_maker.message(ctx, description="Stopped event loop.", send=True)
            self.ukparl_module.tracker_status["loop"] = False
        else:
            await embed_maker.message(
                ctx, description="Event loop is already not running.", send=True
            )

    @mod_cmd_tracker_channels.command(
        name="set",
        help="Set a channel to one of the four trackers",
        usage="uk mod tracker channels set [tracker_id] [channel_id or mention]",
        examples=["uk mod tracker channelts set royalassent #royal-assent"],
        cls=cls.Command,
    )
    async def mod_cmd_tracker_channels_set(
        self,
        ctx: commands.Context,
        tracker_id: str = "",
        channel: TextChannelConverter = None,
    ):
        if self.loaded is False:
            return  # type: ignore

        if tracker_id == "":
            return await embed_maker.command_error(ctx, "tracker_id")

        if channel == "":
            return await embed_maker.command_error(ctx, "channel_id/mention")

        config = self.bot.ukparl_module.config
        channels = config.get_channel_ids()

        if tracker_id.lower() not in channels.keys():
            next_line = "\n"
            return await embed_maker.message(
                ctx,
                description=f"**Valid tracker ids:**{next_line} - {(next_line + ' - ').join(channels.keys)}",
                send=True,
            )

        config.set_channel(tracker_id, channel.id)
        await embed_maker.message(
            ctx, description=f"Set {tracker_id} to channel {channel.name}", send=True
        )

    @uk.command(
        name="mpelection",
        help="View latest election results of an MP by name or constituency name",
        clearence="User",
        usage="uk mpelection [mp name] or uk mpelection <argument identifier> [borough name]",
        examples=[
            "uk mpelection Boris Johnson",
            "uk mpelection --borough Belfast South --nonvoters",
        ],
        cls=cls.Command,
        command_args=[
            (("--borough", "-bo", str), "Name of a borough"),
            (("--nonvoters", "-nv", bool), "Include non-voters in the charts"),
            (
                ("--table", "-t", bool),
                "Whether or not the chart should be a pie or a table",
            ),
            (
                ("--name", "-n", str),
                "The name of the MP (used only if you're using the other arguments",
            ),
            (("--historical", "-h", str), "Get list of recorded election results"),
        ],
    )
    async def mp_elections(
        self, ctx: commands.Context, *, args: Union[ParseArgs, str] = ""
    ):
        if self.loaded is False:
            return
        member = None
        if args == "":
            return await embed_maker.command_error(ctx)
        name_arg = args["pre"] if args["pre"] != "" else args["name"]
        if name_arg != "" and name_arg is not None:
            for m in self.parliament.get_commons_members():
                name = m.get_titled_name()
                if name is None:
                    name = m.get_addressed_name()
                if name is None:
                    name = m.get_display_name()

                if name_arg.lower() in name.lower():
                    member = m
                    break

        elif args["borough"] != "" and args["borough"] is not None:
            for m in self.parliament.get_commons_members():
                if args["borough"].lower() in m.get_membership_from().lower():
                    member = m
                    break

        if member is None:
            return await embed_maker.message(
                ctx,
                description=f"Couldn't find latest elections results for"
                f" {'Borough' if args['borough'] != '' and args['borough'] is not None else 'MP'}"
                f" {args['borough'] if args['borough'] != '' and args['borough'] is not None else args['pre']}",
                send=True,
            )

        next_line = "\n"
        results = await self.parliament.get_election_results(member)
        result = results[0]

        if args["historical"] != "" and args["historical"] is not None:
            historical_bits = [
                f"- {er.get_election_date().strftime('%Y')}" for er in results
            ]
            return await embed_maker.message(
                ctx,
                title=f"Recorded Eletion Results of the Borough {member.get_membership_from()}",
                description=f"**Elections:** {next_line}{next_line.join(historical_bits)}",
                send=True,
            )
        else:
            for h_result in results:
                if h_result.get_election_date().strftime("%Y") == args["historical"]:
                    result = h_result

        others_formatted = []
        the_rest_formatted = []

        for candidate in result.get_candidates():
            if candidate["votes"] > 1000:
                the_rest_formatted.append(
                    f"- {candidate['name']}: {candidate['party_name']}"
                )
            else:
                others_formatted.append(
                    f"- {candidate['name']}: {candidate['party_name']}"
                )

        embed = embeds.Embed = await embed_maker.message(
            ctx,
            title=f'{result.get_election_date().strftime("%Y")} Election Results of {member.get_membership_from()}',
            description=f"**Electorate Size:** {result.get_electorate_size():,}{next_line}"
            f"**Turnout:** {result.get_turnout():,}{next_line}**Main Candidates:**{next_line}"
            f"{next_line.join(the_rest_formatted)}{next_line}**Other Candidates (Under 1k):"
            f"**{next_line}{next_line.join(others_formatted)}"
            if len(others_formatted) > 0
            else "",
        )
        if result is not None:
            image_file = await self.ukparl_module.generate_election_graphic(
                result, args["nonvoters"] is not None, args["table"] is not None
            )
            embed.set_image(url="attachment://electionimage.png")  # type: ignore
            await ctx.send(
                file=image_file,
                embed=embed,
            )
        else:
            await ctx.send(embed=embed)

    @uk.command(
        name="minfo",
        help="Get information on a currently serving MP or Lord",
        usage="uk minfo [mp name / lord name]",
        examples=[
            "uk minfo Boris Johnson",
            "uk minfo Duke of Norfolk",
            "uk minfo -n Lord Sugar -p",
        ],
        command_args=[
            (("--borough", "-b", str), "Get mp info by borough name"),
            (("--portrait", "-p", bool), "Fetch the portrait of the member"),
            (
                ("--name", "-n", str),
                "The name of the mp (used only if you're using the other args",
            ),
        ],
        clearence="User",
        cls=cls.Command,
    )
    async def member_info(
        self, ctx: commands.Context, *, args: Union[ParseArgs, str] = ""
    ):
        if self.loaded is False:
            return  # type: ignore
        if args == "":
            return await embed_maker.command_error(ctx)

        member = None
        members = self.parliament.get_commons_members()
        members.extend(self.parliament.get_lords_members())

        arg_name = args["pre"] if args["pre"] != "" else args["name"]
        if arg_name is not None:
            for m in members:
                name = m.get_titled_name()
                if name is None:
                    name = m.get_addressed_name()
                if name is None:
                    name = m.get_display_name()
                if arg_name.lower() in name.lower():
                    member = m
        else:
            if args["borough"] is not None:
                members = list(
                    filter(
                        lambda m: m.get_membership_from().lower()
                        == args["borough"].lower(),
                        self.parliament.get_commons_members(),
                    )
                )
                if len(members) != 0:
                    member = members[0]

        if member is None:
            await embed_maker.message(
                ctx,
                description="Couldn't find " + arg_name
                if args["pre"] is not None or args["name"] is not None
                else "of borough" + args["borough"] + ".",
                send=True,
            )
            return

        biography = await self.parliament.get_biography(member)
        next_line = "\n"

        representing_bits = []

        for rep in biography.get_representations():
            representing_bits.append(
                (
                    f"- MP for {rep['constituency_name']} from {rep['started'].strftime('%Y-%m-%d')}"
                    f"{' to ' if rep['ended'] is not None else ''}"
                    f"{rep['ended'].strftime('%Y-%m-%d') if rep['ended'] is not None else ''}"
                )
            )

        gov_posts_bits = []

        for post in biography.get_government_posts():
            gov_posts_bits.append(
                (
                    f"- {post['office']} from {post['started'].strftime('%Y-%m-%d')}"
                    f"{' to ' if post['ended'] is not None else ''}"
                    f"{post['ended'].strftime('%Y-%m-%d') if post['ended'] is not None else ''}"
                )
            )

        opp_posts_bits = []

        for post in biography.get_oppositions_posts():
            opp_posts_bits.append(
                (
                    f" - {post['office']} from {post['started'].strftime('%Y-%m-%d')}"
                    f"{' to ' if post['ended'] is not None else ''}"
                    f"{post['ended'].strftime('%Y-%m-%d') if post['ended'] is not None else ''}"
                )
            )

        other_posts_bits = []

        for post in biography.get_other_posts():
            other_posts_bits.append(
                (
                    f" - {post['office']} from {post['started'].strftime('%Y-%m-%d')}"
                    f"{' to ' if post['ended'] is not None else ''}"
                    f"{post['ended'].strftime('%Y-%m-%d') if post['ended'] is not None else ''}"
                )
            )

        cmte_bits = []

        for membership in biography.get_committee_memberships():
            cmte_bits.append(
                (
                    f"- Member of {membership['committee']} from {membership['started'].strftime('%Y-%m-%d')}"
                    f"{' to ' if membership['ended'] is not None else ''}"
                    f"{membership['ended'].strftime('%Y-%m-%d') if membership['ended'] is not None else ''}"
                )
            )

        d = (
            f"**Name:** {member.get_display_name()}{next_line}**"
            f"{'Representing:' if member.get_house() == 1 else 'Peer Type'}** {member.get_membership_from()}"
            f"{next_line}**Gender:** {'Male' if member.get_gender() == 'M' else 'Female'}"
            + (
                f"{next_line}**Represented/Representing:**{next_line}{next_line.join(representing_bits)}"
                if len(representing_bits) > 0
                else ""
            )
            + (
                f"{next_line}**Government Posts**{next_line}{next_line.join(gov_posts_bits)}"
                if len(gov_posts_bits) > 0
                else ""
            )
            + (
                f"{next_line}**Opposition Posts:**{next_line}{next_line.join(opp_posts_bits)}"
                if len(opp_posts_bits) > 0
                else ""
            )
            + (
                f"{next_line}**Other Posts:**{next_line}{next_line.join(other_posts_bits)}"
                if len(other_posts_bits) > 0
                else ""
            )
            + (
                f"{next_line}**Committee Posts:**{next_line}{next_line.join(cmte_bits)}"
                if len(cmte_bits) > 0
                else ""
            )
        )

        embed: embeds.Embed = await embed_maker.message(ctx, description=d)  # type: ignore

        if args["portrait"] is not None:
            url = (
                member.get_thumbnail_url().replace("Thumbnail", "Portrait")
                + "?cropType=FullSize&webVersion=false"
            )
            portrait_image = await self.ukparl_module.get_mp_portrait(url)
            if portrait_image is not None:
                embed.set_image(url=f"attachment://portrait.jpeg")
                await ctx.send(file=portrait_image, embed=embed)
        else:
            await ctx.send(embed=embed)

    @divisions.command(
        name="linfo",
        help="Get House of Lords Division information",
        usage="uk divisions linfo [division id]",
        examples=["uk divisions linfo 1234"],
        clearence="User",
        cls=cls.Command,
    )
    async def division_lord_info(self, ctx: commands.Context, division_id: int = -1):
        if self.loaded is False:
            return  # type: ignore

        if division_id == -1:
            await embed_maker.command_error(ctx, "division_id")
            return

        division = await self.parliament.get_lords_division(division_id)
        if division is None:
            await embed_maker.message(
                ctx,
                description=f"Couldn't find division under id {division_id}",
                send=True,
            )

        division_image = await self.ukparl_module.generate_division_image(
            self.parliament, division
        )
        next_line = "\n"
        did_pass = division.get_aye_count() > division.get_no_count()
        embed: embeds.Embed = await embed_maker.message(
            ctx,
            description=f"**Title:** {division.get_division_title()}{next_line}"
            f"**Division Outcome:** {'Passed' if did_pass else 'Not passed'} by a division of "
            f"{division.get_aye_count() if did_pass else division.get_no_count()} {'Ayes' if did_pass else 'Noes'}"
            f" to {division.get_no_count() if did_pass else division.get_aye_count()} {'Noes' if did_pass else 'Ayes'}"
            f"{next_line}**Division Date:** {division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}{next_line}"
            f"**Summary:** {division.get_amendment_motion_notes()[0:250]}",
        )  # type: ignore
        embed.set_image(url="attachment://divisionimage.png")
        await ctx.send(file=division_image, embed=embed)

    @divisions.command(
        name="cinfo",
        help="Get House of Commons Division information",
        usage="uk divisions cinfo [division id]",
        examples=["uk divisions cinfo 1234"],
        clearence="User",
        cls=cls.Command,
    )
    async def division_common_info(self, ctx: commands.Context, division_id: int):
        if self.loaded is False:
            return  # type: ignore
        division = await self.parliament.get_commons_division(division_id)
        if division is None:
            await embed_maker.message(
                ctx,
                description=f"Couldn't find division under id {division_id}",
                send=True,
            )

        image_file = await self.ukparl_module.generate_division_image(
            self.parliament, division
        )
        next_line = "\n"
        did_pass = division.get_aye_count() > division.get_no_count()
        embed: embeds.Embed = await embed_maker.message(
            ctx,
            description=f"**Title:** {division.get_division_title()}{next_line}**Division Outcome:**"
            f" {'Passed' if did_pass else 'Not passed'} by a division of "
            f"{division.get_aye_count() if did_pass else division.get_no_count()} {'Ayes' if did_pass else 'Noes'} "
            f"to {division.get_no_count() if did_pass else division.get_aye_count()} {'Noes' if did_pass else 'Ayes'}"
            f"{next_line}**Division Date:** {division.get_division_date().strftime('%Y-%m-%d %H:%M:%S')}",
        )  # type: ignore

        embed.set_image(url=f"attachment://{image_file.filename}")
        await ctx.send(file=image_file, embed=embed)

    @divisions.command(
        name="csearch",
        help="Search for commons divisions",
        usage="uk divisions csearch [search term]",
        examples=["uk divisions csearch European"],
        clearence="User",
        cls=cls.Command,
    )
    async def division_commons_search(self, ctx: commands.Context, *, search_term=""):
        if self.loaded is False:
            return  # type: ignore
        divisions = await self.parliament.search_for_commons_divisions(
            search_term, result_limit=30
        )
        if len(divisions) == 0:
            await embed_maker.message(
                ctx,
                description=f"Couldn't find any Commons divisions under search term '{search_term}'.",
                send=True,
            )

        page_constructor = functools.partial(
            self.construct_divisions_commons_embed,
            ctx=ctx,
            divisions=divisions,
            page_limit=5,
        )
        pair = await page_constructor(page=1)
        message = await ctx.send(embed=pair[0])

        async def temp_page_constructor(page: int):
            pair = await page_constructor(page=page)
            return pair[0]

        menu = BookMenu(
            message=message,
            author=ctx.author,  # type: ignore
            max_page_num=pair[1],
            page_constructor=temp_page_constructor,
            page=1,
        )
        self.bot.reaction_menus.add(menu)

    @divisions.command(
        name="lsearch",
        help="Search for lords divisions",
        usage="uk divisions lsearch [search term]",
        examples=["uk divisions lsearch European"],
        clearence="User",
        cls=cls.Command
    )
    async def division_lords_search(self, ctx: commands.Context, *, search_term=""):
        if self.loaded is False:
            return  # type: ignore
        divisions = await self.parliament.search_for_lords_divisions(
            search_term, result_limit=30
        )

        if len(divisions) == 0:
            await embed_maker.message(
                ctx,
                description=f"Couldn't find any Lords divisions under the search term '{search_term}'.",
                send=True,
            )
            return

        page_constructor = functools.partial(
            self.construct_divisions_lords_embed,
            ctx=ctx,
            divisions=divisions,
            page_limit=5,
        )
        pair = await page_constructor(page=1)
        embed = pair[0]
        max_pages = pair[1]

        async def temp_page_constructor(page: int):
            pair = await page_constructor(page=page)
            return pair[0]

        message = await ctx.send(embed=embed)
        menu = BookMenu(
            message=message,
            author=ctx.author,  # type: ignore
            max_page_num=max_pages,
            page_constructor=temp_page_constructor,
            page=1,
        )
        self.bot.reaction_menus.add(menu)

    @bills.command(
        name="info",
        help="To display in more detail information about a bill.",
        clearence="User",
        usage="uk bills info [bill id]",
        examples=["uk bills info 1234"],
        cls=cls.Command,
    )
    async def bill_info(self, ctx: commands.Context, bill_id: int):
        if self.loaded is False:
            return  # type: ignore
        try:
            bill = await self.parliament.get_bill(bill_id)
            c_divisions = await self.parliament.search_for_commons_divisions(
                bill.get_title()
            )
            l_divisions = await self.parliament.search_for_lords_divisions(
                bill.get_title()
            )
            page_constructor = functools.partial(
                self.construct_bill_info_embed,
                ctx=ctx,
                bill=bill,
                l_divisions=l_divisions,
                c_divisions=c_divisions,
                page_limit=10,
            )
            pair: tuple[embeds.Embed, int] = await page_constructor(page=1)
            message = await ctx.send(embed=pair[0])

            async def temp_page_constructor(
                page: int,
            ):  # Due to the unique nature of ther result, this is needed ot return only the embed.
                pair = await page_constructor(page=page)
                return pair[0]

            menu = BookMenu(
                message,
                author=ctx.author,  # type: ignore
                page=1,
                max_page_num=pair[1],
                page_constructor=temp_page_constructor,
            )
            self.bot.reaction_menus.add(menu)

        except Exception as ignore:
            await embed_maker.message(
                ctx, description=f"Couldn't fetch bill {bill_id}.", send=True
            )
            raise ignore

    @bills.command(
        help="Seach for bills using certain values and search terms",
        usage="uk bills search [search terms]",
        examples=[
            "uk bills search European Withdrawal",
            "uk bills search --query Finance Bill --currenthouse Lords",
            "uk bills search --sponsor Rishu Sunak",
        ],
        name="search",
        clearence="User",
        command_args=[
            (("--query", "-q", str), "Search Term to search for"),
            (("--sponsor", "-s", str), "The name of the bill sponsor"),
            (("--types", None, "-t"), "The types of bill to search for"),
            (("--order", None, "-o"), "The order to display the searches in"),
            (("--currenthouse", "-ch", str), "The house the bill is currently in"),
            (("--originatinghouse", "-oh", str), "The house the bill originated in"),
        ],
        cls=cls.Command,
    )
    async def bills_search(
        self, ctx: commands.Context, *, args: Union[ParseArgs, str] = ""
    ):
        if self.loaded is False:
            return  # type: ignore

        if args == "":
            return await embed_maker.command_error(ctx)
        builder = SearchBillsBuilder.builder()

        if args is None:
            return

        if args["pre"] is not None:
            builder.set_search_term(args["pre"])
        else:
            if args["query"] is not None:
                builder.set_search_term(args["query"])
            if args["sponsor"] is not None:
                member = self.parliament.get_member_by_name(args["sponsor"])
                if member is None:
                    await embed_maker.message(
                        ctx,
                        description=f"Couldn't find member {args['sponsor']}",
                        send=True,
                    )
                builder.set_member_id(member.get_id())
            if args["types"] is not None:
                split_types = args["types"].split(" ")
                types = self.parliament.get_bill_types()
                arg_types = []
                for t_type in split_types:
                    for b_type in types:
                        if b_type.get_name().lower() == t_type.lower():
                            arg_types.append(b_type)
                builder.set_bill_type(arg_types)
            if args["order"] is not None:
                formatted_acceptable_args = list(
                    map(lambda order: order.name.lower(), SearchBillsSortOrder)
                )
                next_line = "\n"
                await embed_maker.message(
                    ctx,
                    description=f"Couldn't find order type {args['order']}. Acceptable arguments: {next_line}"
                    f"{next_line.join(formatted_acceptable_args)}",
                    send=True,
                )
            if args["currenthouse"] is not None:
                if args["currenthouse"].lower() not in [
                    "all",
                    "commons",
                    "lords",
                    "unassigned",
                ]:
                    await embed_maker.message(
                        ctx,
                        description="Incorrect house value. "
                        "Accepted arguments: 'all', 'commons', 'lords', 'unassigned'",
                        send=True,
                    )
                    return
                builder.set_current_house(args["currenthouse"])
            if args["originatinghouse"] is not None:
                if args["originatinghouse"] not in [
                    "all",
                    "commons",
                    "lords",
                    "unassigned",
                ]:
                    await embed_maker.message(
                        ctx,
                        description="Incorrect house value. "
                        "Accepted arguments: 'all', 'commons', 'lords', 'unassigned'",
                        send=True,
                    )
                    return
                builder.set_originating_house(args["originatinghouse"])

        bills = await self.parliament.search_bills(
            builder.set_sort_order(SearchBillsSortOrder.DATE_UPDATED_DESENDING).build()
        )
        next_line = "\n"

        max_page_size = 4
        max_page_num = math.ceil(len(bills) / max_page_size)
        if max_page_num == 0:
            max_page_num = 1
        page_constructor = functools.partial(
            self.construct_bills_search_embed,
            ctx=ctx,
            bills=bills,
            max_page_num=max_page_num,
            page_limit=max_page_size,
        )

        embed = await page_constructor(page=1)
        message = await ctx.send(embed=embed)
        menu = BookMenu(
            message,
            author=ctx.author,  # type: ignore
            page=1,
            max_page_num=max_page_num,
            page_constructor=page_constructor,
        )
        self.bot.reaction_menus.add(menu)
示例#3
0
class Leveling(Cog):
    def __init__(self, bot: TLDR):
        self.bot = bot

        # parliamentary points earn cooldown
        self.pp_cooldown = Cooldown()
        self.hp_cooldown = Cooldown()

    @command(
        help='Show someone you respect them by giving them a reputation point',
        usage='rep [member] [reason for the rep]',
        examples=['rep @Hattyot for being an excellent example in this text'],
        cls=commands.Command,
        aliases=['reputation']
    )
    async def rep(self, ctx: Context, *, member_reason: str = None):
        # check if user has been in server for more than 7 days
        now_datetime = datetime.datetime.now()
        joined_at = ctx.author.joined_at
        diff = now_datetime - joined_at
        if round(diff.total_seconds()) < 86400 * 7:  # 7 days
            return await embed_maker.error(ctx, f'You need to be on this server for at least 7 days to give rep points')

        # check if user can give rep point
        giving_leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, ctx.author.id)
        if not giving_leveling_member.rep_timer_expired:
            time_left = giving_leveling_member.rep_time_left
            return await embed_maker.message(
                ctx,
                description=f'You can give someone a reputation point again in:\n'
                            f'**{format_time.seconds(time_left, accuracy=3)}**',
                send=True
            )

        if member_reason is None:
            return await embed_maker.command_error(ctx)

        receiving_member, reason = await get_member_from_string(ctx, member_reason)

        if receiving_member is None:
            return await embed_maker.error(ctx, 'Invalid member')

        if not reason:
            return await embed_maker.command_error(ctx, '[reason for the rep]')

        if receiving_member.id == ctx.author.id:
            return await embed_maker.error(ctx, f'You can\'t give rep points to yourself')

        if receiving_member.bot:
            return await embed_maker.error(ctx, f'You can\'t give rep points to bots')

        # check last rep
        if giving_leveling_member.last_rep == receiving_member.id:
            return await embed_maker.error(ctx, f'You can\'t give rep to the same person twice in a row')

        receiving_leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, receiving_member.id)

        # set rep_time to 24h so user cant spam rep points
        expire = round(time.time()) + 86400  # 24 hours
        giving_leveling_member.rep_timer = expire
        # log who user gave the rep_point to, so that a person can't rep the same person twice in a row
        giving_leveling_member.last_rep = receiving_member.id

        # give member rep point
        receiving_leveling_member.rp += 1

        await embed_maker.message(ctx, description=f'Gave +1 rep to <@{receiving_member.id}>', send=True)

        # send member rep reason
        msg = f'<@{ctx.author.id}> gave you a reputation point:\n**"{reason}"**'
        embed = await embed_maker.message(
            ctx,
            description=msg,
            author={'name': 'Rep'}
        )

        # try except because bot might not be able to dm member
        try:
            await receiving_member.send(embed=embed)
        except Exception:
            pass

        # start rep timer if giving leveling_member has rep@ enabled
        if giving_leveling_member.settings.rep_at:
            self.bot.timers.create(
                guild_id=ctx.guild.id,
                expires=round(time.time()) + 86400,  # 24 hours
                event='rep_at',
                extras={
                    'member_id': giving_leveling_member.id
                }
            )

        # check if user already has rep boost, if they do, extend it by 30 minutes, otherwise add 10% boost for 6h
        if not receiving_leveling_member.boosts.rep.has_expired():
            boost = receiving_leveling_member.boosts.rep
            # if boost is expired or boost + 30min is bigger than 6 hours set expire to 6 hours
            if boost.expires < round(time.time()) or (boost.expires + 1800) - round(time.time()) > (3600 * 6):
                expires = round(time.time()) + (3600 * 6)
            # otherwise just expand expire by 30 minutes
            else:
                expires = boost.expires + 1800  # 30 min
        else:
            expires = round(time.time()) + (3600 * 6)

        # give boost to to receiving leveling member
        receiving_leveling_member.boosts.rep.expires = expires
        receiving_leveling_member.boosts.rep.multiplier = 0.1

    @group(
        invoke_without_command=True,
        help='See current leveling routes',
        usage='leveling_routes (branch <parliamentary/honours>)',
        examples=[
            'ranks',
            'ranks parliamentary',
            'ranks honours'
        ],
        Admins=commands.Help(
            help='See current leveling routes or add, remove or edit roles',
            usage='ranks (branch) (sub command) (args)',
            examples=['ranks', 'ranks honours'],
        ),

        cls=commands.Group
    )
    async def ranks(self, ctx: Context, branch: str = 'parliamentary'):
        if ctx.subcommand_passed is None and branch:
            leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)
            branch = leveling_guild.get_leveling_route(branch)

            embed = await embed_maker.message(ctx, author={'name': 'Ranks'})

            # Looks up how many people have a role
            count = {
                role.name: db.leveling_users.count({'guild_id': ctx.guild.id, f'{branch.name[0]}_role': role.name, f'{branch.name[0]}p': {'$gt': 0}})
                for role in branch.roles
            }

            value = ''
            for i, role in enumerate(branch.roles):
                guild_role = await role.get_guild_role()
                value += f'\n**#{i + 1}:** <@&{guild_role.id}> - {count[guild_role.name]} ' + ('People' if count[guild_role.name] != 1 else 'Person')

            if not value:
                value = 'This branch currently has no roles'

            amount_of_people = sum(count.values())
            value += f'\n\nTotal: **{amount_of_people} ' + ('People**' if amount_of_people != 1 else 'Person**')
            embed.add_field(name=f'>{branch.name.title()} - Every 5 levels you advance a role', value=value, inline=False)

            return await ctx.send(embed=embed)

    @ranks.command(
        name='add',
        help='Add a role to parliamentary or honours route',
        usage='ranks add [branch] [role name]',
        examples=['ranks add honours Pro', 'ranks add honours Knight 2'],
        cls=commands.Command
    )
    async def ranks_add(self, ctx: Context, branch: str = None, *, role_name: str = None):
        if branch is None:
            return await embed_maker.command_error(ctx)

        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)
        branch = leveling_guild.get_leveling_route(branch)
        if branch is None:
            branch = leveling_guild.leveling_routes.parliamentary

        if not role_name:
            return await embed_maker.command_error(ctx, '[role name]')

        new_role = {
            'name': role_name,
            'perks': []
        }

        leveling_role = leveling.LevelingRole(ctx.guild, branch, new_role)
        branch.roles.append(leveling_role)

        await embed_maker.message(
            ctx,
            description=f'`{role_name}` has been added to the list of `{branch.name}` roles',
            colour='green',
            send=True
        )

        ctx.invoked_subcommand = ''
        return await self.ranks(ctx, branch.name)

    @ranks.command(
        name='remove',
        help='Remove a role from the list of parliamentary or honours roles',
        usage='ranks remove [branch] [role name]',
        examples=['ranks remove honours Knight'],
        cls=commands.Command
    )
    async def ranks_remove(self, ctx: Context, branch: str = None, *, role: str):
        if not branch or type(branch) == int:
            return await embed_maker.command_error(ctx)

        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)
        leveling_role = leveling_guild.get_leveling_role(role)

        branch = leveling_guild.get_leveling_route(branch)
        if branch is None:
            branch = leveling_guild.leveling_routes.parliamentary

        if leveling_role is None:
            return await embed_maker.error(ctx, f"Couldn't find a {branch.name} role by the name {role}")

        branch.roles.remove(leveling_role)

        await embed_maker.message(
            ctx,
            description=f'`{leveling_role.name}` has been remove from the list of `{branch.name}` roles',
            colour='green',
            send=True
        )

        ctx.invoked_subcommand = ''
        return await self.ranks(ctx, branch.name)

    @group(
        invoke_without_command=True,
        help='See all the perks that a role has to offer',
        usage='perks (role name)',
        examples=['perks', 'perks Party Member'],
        Moderators=commands.Help(
            help='See all the perks that a role has to offer or add or remove them',
            usage='perks (<sub command/role name>) (args)',
            examples=['perks', 'perks Party Member'],
        ),

        cls=commands.Group
    )
    async def perks(self, ctx: Context, *, role: str = None):
        if ctx.subcommand_passed is None:
            leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)

            leveling_routes = leveling_guild.leveling_routes
            honours_branch = leveling_routes.honours
            parliamentary_branch = leveling_routes.parliamentary

            if role is None:
                # find roles that have perks
                filtered_parliamentary = list(filter(lambda r: r.perks, parliamentary_branch))
                filtered_honours = list(filter(lambda r: r.perks, honours_branch))

                embed = await embed_maker.message(
                    ctx,
                    description=f'To view perks of a role type `{ctx.prefix}perks [role name]`',
                    author={'name': f'List of roles with perks'},
                )

                if filtered_parliamentary:
                    parliamentary_str = '\n'.join(r.name for r in filtered_parliamentary)
                else:
                    parliamentary_str = 'Currently no Parliamentary roles offer any perks'

                if filtered_honours:
                    honours_str = '\n'.join(r.name for r in filtered_honours)
                else:
                    honours_str = 'Currently no Honours roles offer any perks'

                embed.add_field(name='>Parliamentary Roles With Perks', value=parliamentary_str, inline=False)
                embed.add_field(name='>Honours Roles With Perks', value=honours_str, inline=False)

                return await ctx.send(embed=embed)

            if role:
                leveling_role = leveling_guild.get_leveling_role(role)
                if not leveling_role.perks:
                    perks_str = f'**{leveling_role.name}** currently offers no perks'
                else:
                    perks_str = "\n".join([f'`#{i + 1}` - {perk}' for i, perk in enumerate(leveling_role.perks)])

                return await embed_maker.message(
                    ctx,
                    description=perks_str,
                    author={'name': f'{leveling_role.name} - Perks'},
                    send=True
                )

    async def modify_perks(self, ctx: Context, command: str, args: dict, message: str) -> Optional[Union[dict, discord.Message]]:
        if args is None:
            return await embed_maker.command_error(ctx)

        role_name = args['role']
        new_perks = args['perk']

        if not role_name:
            return await embed_maker.error(ctx, "Missing role arg")

        if not new_perks and command != 'pull':
            return await embed_maker.error(ctx, "Missing perks arg")

        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)
        leveling_role = leveling_guild.get_leveling_role(role_name)
        if not leveling_role:
            return await embed_maker.error(ctx, 'Invalid role provided.')

        if command == 'add':
            leveling_role.perks += new_perks
        elif command == 'set':
            leveling_role.perks = new_perks
        elif command == 'remove' and not new_perks:
            leveling_role.perks = []
        elif command == 'remove' and new_perks:
            to_remove = [int(num) - 1 for num in new_perks if num.isdigit() and 0 < int(num) <= len(leveling_role.perks)]
            leveling_role.perks = [perk for i, perk in enumerate(leveling_role.perks) if i not in to_remove]

        await embed_maker.message(
            ctx,
            description=message.format(**{'role': leveling_role}),
            colour='green',
            send=True
        )

        # send embed of role perks
        perks_str = "\n".join([f'`#{i + 1}` - {perk}' for i, perk in enumerate(leveling_role.perks)])

        return await embed_maker.message(
            ctx,
            description=perks_str,
            author={'name': f'{leveling_role.name} - New Perks'},
            send=True
        )

    @perks.command(
        name='set',
        help='set the perks of a role',
        usage='perks set [args]',
        examples=['perks set -r Party Member -p Monthly giveaways -p some cool perk'],
        command_args=[
            (('--role', '-r', str), 'The name of the role you want to set the perks for'),
            (('--perk', '-p', list), 'Perk for the role')
        ],
        cls=commands.Command
    )
    async def perks_set(self, ctx: Context, *, args: Union[ParseArgs, dict] = None):
        return await self.modify_perks(ctx, 'set', args, 'New perks have been set for role `{role.name}`')

    @perks.command(
        name='add',
        help='Add perks to a role',
        usage='perks add [args]',
        examples=['perks add -r Party Member -p Monthly giveaways -p some cool perk'],
        command_args=[
            (('--role', '-r', str), 'The name of the role you want to add the perks to'),
            (('--perk', '-p', list), 'Perk for the role')
        ],
        cls=commands.Command
    )
    async def perks_add(self, ctx: Context, *, args: Union[ParseArgs, dict] = None):
        return await self.modify_perks(ctx, 'add', args, 'New perks have been added for role `{role.name}`')

    @perks.command(
        name='remove',
        help='remove perks of a role, or remove only some perks',
        usage='perks remove -r [role name] -p (perk number)',
        examples=[
            'perks remove -r Party Member -p 1 -p 2',
            'perks remove -r Party Member'
        ],
        command_args=[
            (('--role', '-r', str), 'The name of the role you want to remove perks from'),
            (('--perk', '-p', list), 'Index of the perk you want to remove, can be seen by doing >perks [role]')
        ],
        cls=commands.Command
    )
    async def perks_remove(self, ctx: Context, *, args: Union[ParseArgs, dict] = None):
        return await self.modify_perks(ctx, 'remove', args, 'Perks have been removed from role `{role.name}`')

    @group(
        invoke_without_command=True,
        help='See all the honours channels',
        usage='honours_channel',
        examples=['honours_channels'],

        Admins=commands.Help(
            help='See current honours channels or add or remove them',
            usage='honours_channel (action - <add/remove>) [#channel]',
            examples=['honours_channels', 'honours_channels add #court', 'honours_channels remove #Mods'],
        ),
        cls=commands.Group
    )
    async def honours_channels(self, ctx: Context):
        if ctx.subcommand_passed is None:
            leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)

            # display list of honours channels
            channel_list_str = ', '.join(f'<#{channel}>' for channel in leveling_guild.honours_channels) if leveling_guild.honours_channels else 'None'
            return await embed_maker.message(ctx, description=channel_list_str, send=True)

    @honours_channels.command(
        name='add',
        help='Add an honours channel',
        usage='honours_channels add [#channel]',
        examples=['honours_channels add #Tech-Lobby'],
        cls=commands.Command
    )
    async def honours_channels_add(self, ctx: Context, channel=None):
        if channel is None:
            return await embed_maker.command_error(ctx)

        if not channel:
            return await embed_maker.command_error(ctx, '[#channel]')

        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)

        if channel.id not in leveling_guild.honours_channels:
            return await embed_maker.message(ctx, description='That channel is not on the list', colour='red', send=True)

        leveling_guild.honours_channels.remove(channel.id)
        msg = f'<#{channel.id}> has been removed from the list of honours channels'
        return await embed_maker.message(ctx, description=msg, colour='green', send=True)

    @honours_channels.command(
        name='remove',
        help='Remove an honours channel',
        usage='honours_channels remove [#channel]',
        examples=['honours_channels remove #Tech-Lobby'],
        cls=commands.Command
    )
    async def honours_channels_remove(self, ctx: Context, channel=None):
        if channel is None:
            return await embed_maker.command_error(ctx)

        if not channel:
            return await embed_maker.command_error(ctx, '[#channel]')

        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)

        if channel.id in leveling_guild.honours_channels:
            return await embed_maker.message(ctx, description='That channel is already on the list', colour='red', send=True)

        leveling_guild.honours_channels.append(channel.id)
        msg = f'<#{channel.id}> has been added to the list of honours channels'
        return await embed_maker.message(ctx, description=msg, colour='green', send=True)

    @command(
        help='See how many messages you need to send to level up and rank up or see how many messages until you reach a level (dont forget about the 60s cooldown)',
        usage='mlu (level)',
        examples=['mlu', 'mlu 60'],
        cls=commands.Command
    )
    async def mlu(self, ctx: Context, level: Union[float, str] = None):
        # incase somebody typed something that isnt a level
        if type(level) == str:
            level = None

        leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, ctx.author.id)
        user_level = leveling_member.parliamentary.level
        points = leveling_member.parliamentary.points
        if not level:
            # points needed until level_up
            pp_till_next_level = round((5 / 6) * (user_level + 1) * (2 * (user_level + 1) * (user_level + 1) + 27 * (user_level + 1) + 91)) - points
            avg_msg_needed = math.ceil(pp_till_next_level / 20)

            # points needed to rank up
            user_rank = leveling_member.user_role_level(leveling_member.parliamentary)
            missing_levels = 6 - user_rank

            rank_up_level = user_level + missing_levels
            pp_needed_rank_up = round((5 / 6) * rank_up_level * (2 * rank_up_level * rank_up_level + 27 * rank_up_level + 91)) - points
            avg_msg_rank_up = math.ceil(pp_needed_rank_up / 20)
            description = f'Messages needed to:\n'\
                          f'Level up: **{avg_msg_needed}**\n'\
                          f'Rank up: **{avg_msg_rank_up}**'
        else:
            pp_needed = round((5 / 6) * level * (2 * level * level + 27 * level + 91)) - points
            avg_msg_needed = math.ceil(pp_needed / 20)
            description = f'Messages needed to reach level `{level}`: **{avg_msg_needed}**'

        return await embed_maker.message(
            ctx,
            description=description,
            author={'name': 'MLU'},
            send=True
        )

    async def construct_lb_str(self, ctx: Context, branch: leveling.LevelingRoute, sorted_users: list, index: int, your_pos: bool = False):
        lb_str = ''
        for i, leveling_user in enumerate(sorted_users):
            leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, leveling_user['user_id'])
            addition = 0 if your_pos else 1

            member = leveling_member.member
            if leveling_member.id == ctx.author.id:
                lb_str += rf'**`#{index + i + addition}`**\* - {member.display_name}'
            else:
                lb_str += f'`#{index + i + addition}` - {member.display_name}'

            if not your_pos:
                lb_str += f'  [{member}]'

            pre = '\n' if not your_pos else ' | '

            # parliamentary and honours branches need different things from rep
            if branch.name[0] in ['p', 'h']:
                user_branch = leveling_member.parliamentary if branch.name[0] == 'p' else leveling_member.honours
                user_role = await leveling_member.guild.get_leveling_role(user_branch.role).get_guild_role()
                role_level = leveling_member.user_role_level(user_branch)
                progress_percent = leveling_member.percent_till_next_level(user_branch)

                lb_str += f'{pre}**Level {role_level}** <@&{user_role.id}> | Progress: **{progress_percent}%**\n'
                if not your_pos:
                    lb_str += '\n'
            else:
                lb_str += f' | **{leveling_member.rp} Reputation**\n'

        return lb_str

    async def construct_lb_embed(self, ctx: Context, branch: leveling.LevelingRoute, user_index: int, sorted_users: list, page_size_limit: int, max_page_num: int, *, page: int):
        sorted_users_page = sorted_users[page_size_limit * (page - 1):page_size_limit * page]
        leaderboard_str = await self.construct_lb_str(ctx, branch, sorted_users_page, index=page_size_limit * (page - 1))
        description = 'Damn, this place is empty' if not leaderboard_str else leaderboard_str

        leaderboard_embed = await embed_maker.message(
            ctx,
            description=description,
            footer={'text': f'{ctx.author} | Page {page}/{max_page_num}'},
            author={'name': f'{branch.name.title()} Leaderboard'}
        )

        # Displays user position under leaderboard and users above and below them if user is below position 10
        if user_index < len(sorted_users) and not (user_index + 1 <= page * page_size_limit):
            sorted_users_segment = sorted_users[user_index - 1:user_index + 2]
            your_pos_str = await self.construct_lb_str(ctx, branch, sorted_users_segment, user_index, your_pos=True)
            leaderboard_embed.add_field(name='Your Position', value=your_pos_str)

        return leaderboard_embed

    @command(
        help='Shows the leveling leaderboards (parliamentary(p)/honours(h)) on the server',
        usage='leaderboard (branch)',
        aliases=['lb'],
        examples=['leaderboard parliamentary', 'lb honours'],
        cls=commands.Command
    )
    async def leaderboard(self, ctx: Context, branch: str = 'parliamentary', page: int = 1):
        leveling_guild = self.bot.leveling_system.get_guild(ctx.guild.id)
        leveling_routes = leveling_guild.leveling_routes

        if branch.isdigit():
            page = int(branch)
            branch = leveling_guild.leveling_routes.parliamentary
        else:
            branch_switch = {'p': leveling_routes.parliamentary, 'h': leveling_routes.honours, 'r': leveling_routes.reputation}
            branch = branch_switch.get(branch[0], leveling_routes.parliamentary)

        key = f'{branch.name[0]}p'
        # get list of users sorted by points who have more than 0 points
        sorted_users = [u for u in db.leveling_users.find({'guild_id': ctx.guild.id, key: {'$gt': 0}}).sort(key, -1)]

        page_size_limit = 10

        # calculate max page number
        max_page_num = math.ceil(len(sorted_users) / page_size_limit)
        if max_page_num == 0:
            max_page_num = 1

        if page > max_page_num:
            return await embed_maker.error(ctx, 'Exceeded maximum page number')

        leveling_user = db.get_leveling_user(ctx.guild.id, ctx.author.id)
        user_index = sorted_users.index(leveling_user) if leveling_user in sorted_users else len(sorted_users)

        # create function with all the needed values except page, so the function can be called with only the page kwarg
        page_constructor = functools.partial(
            self.construct_lb_embed,
            ctx,
            branch,
            user_index,
            sorted_users,
            page_size_limit,
            max_page_num
        )

        # make and send initial leaderboard page
        leaderboard_embed = await page_constructor(page=page)
        leaderboard_message = await ctx.send(embed=leaderboard_embed)

        menu = BookMenu(
            leaderboard_message,
            author=ctx.author,
            page=page,
            max_page_num=max_page_num,
            page_constructor=page_constructor,
            extra_back=5,
            extra_forward=5
        )

        self.bot.reaction_menus.add(menu)

    async def branch_rank_str(self, user_branch: leveling.LevelingUserBranch, leveling_member: leveling.LevelingMember, verbose: bool):
        role_level = leveling_member.user_role_level(user_branch)

        # calculate user rank by counting users who have more points than user
        rank = leveling_member.rank(user_branch)

        leveling_role = leveling_member.guild.get_leveling_role(user_branch.role)
        if leveling_role is None:
            return

        guild_role = await leveling_role.get_guild_role()

        progress = leveling_member.percent_till_next_level(user_branch)

        if verbose:
            points_till_next_level = round(5 / 6 * (user_branch.level + 1) * (2 * (user_branch.level + 1) * (user_branch.level + 1) + 27 * (user_branch.level + 1) + 91))
            cooldown_object = self.pp_cooldown

            cooldown = f'{cooldown_object.user_cooldown(leveling_member.guild.id, leveling_member.id)} seconds'

            rank_str = f'**Rank:** `#{rank}`\n' \
                       f'**Role:** <@&{guild_role.id}>\n' \
                       f'**Role Level:** {role_level}\n' \
                       f'**Total Level:** {user_branch.level}\n' \
                       f'**Points:** {user_branch.points}/{points_till_next_level}\n' \
                       f'**Progress:** {progress}%\n' \
                       f'**Cooldown**: {cooldown}'
        else:
            rank_str = f'**#{rank}** | **Level** {role_level} <@&{guild_role.id}> | Progress: **{progress}%**'

        return rank_str

    @staticmethod
    async def rep_rank_str(leveling_member: leveling.LevelingMember, verbose: bool):
        # this is kind of scuffed, but it works
        rank = leveling_member.rank(leveling_member.reputation)
        if verbose:
            rep_time = int(leveling_member.rep_timer) - round(time.time())
            if rep_time < 0:
                rep_time = 0

            rep_time_str = format_time.seconds(rep_time, accuracy=10)

            last_rep = f'<@{leveling_member.last_rep}>' if leveling_member.last_rep else 'None'
            rep_str = f"**Reputation:** {leveling_member.rp}\n" \
                      f"**Rep timer:** {rep_time_str}\n" \
                      f"**Last Rep: {last_rep}**\n"
        else:
            rep_str = f'**#{rank}** | **{leveling_member.rp}** reputation'

        return rep_str

    @command(
        help='Shows your (or someone else\'s) rank and level, add -v too see all the data',
        usage='rank (member) (-v)',
        examples=['rank', 'rank @Hattyot', 'rank Hattyot -v'],
        cls=commands.Command
    )
    async def rank(self, ctx: Context, *, user_input: str = ''):
        verbose = bool(re.findall(r'(?:\s|^)(-v)(?:\s|$)', user_input))

        user_input = re.sub(r'((\s|^)-v(\s|$))', '', user_input)

        # set member to author if user_input is just -v or user_input isn't provided
        if not user_input:
            member = ctx.author
        else:
            member = await get_member(ctx, user_input)
            if type(member) == discord.Message:
                return

        if member.bot:
            return await embed_maker.error(ctx, 'No bots allowed >:(')

        rank_embed = await embed_maker.message(
            ctx,
            footer={'text': str(member), 'icon_url': member.avatar_url},
            author={'name': f'{member.name} - Rank'}
        )

        leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, member.id)

        # inform user of boost, if they have it
        boost_multiplier = leveling_member.boosts.get_multiplier()
        if boost_multiplier > 1:
            boost_percent = round((boost_multiplier - 1) * 100, 1)

            if boost_percent.is_integer():
                boost_percent = int(boost_percent)

            rank_embed.description = f'Active boost: **{boost_percent}%** parliamentary points gain!'

        if leveling_member.honours.points > 0:
            user_branch = leveling_member.honours
            hp_str = await self.branch_rank_str(user_branch, leveling_member, verbose)
            rank_embed.add_field(name='>Honours', value=hp_str, inline=False)

        user_branch = leveling_member.parliamentary
        pp_str = await self.branch_rank_str(user_branch, leveling_member, verbose)
        rank_embed.add_field(name='>Parliamentary', value=pp_str, inline=False)

        if leveling_member.rp:
            rep_str = await self.rep_rank_str(leveling_member, verbose)
            rank_embed.add_field(name='>Reputation', value=rep_str, inline=False)

        # add boosts field
        if verbose and leveling_member.boosts:
            boost_str = ''
            for i, boost in enumerate(leveling_member.boosts):
                if boost.has_expired():
                    boost.remove()

                percent = round(boost.multiplier * 100, 1)
                boost_str += f'`#{i + 1}` - {percent}% boost | Expires: {format_time.seconds(boost.expires - round(time.time()), accuracy=5)}'
                boost_str += f' | Type: {boost.boost_type}\n'

            boost_str = 'None' if not boost_str else boost_str
            rank_embed.add_field(name='>Boosts', value=boost_str)

        if verbose:
            rank_embed.add_field(name='>Settings', value=f'**@me:** {leveling_member.settings.at_me}', inline=False)

        return await ctx.send(embed=rank_embed)

    @command(
        help='Toggle the different leveling settings.',
        usage='settings (setting)',
        examples=["settings @me", "settings rep@"],
        cls=commands.Command
    )
    async def settings(self, ctx: Context, *, setting: str = None):
        embed = await embed_maker.message(ctx, description=f"To toggle a setting, type `{ctx.prefix}settings [setting]`", author={'name': 'Settings'})

        leveling_member = await self.bot.leveling_system.get_member(ctx.guild.id, ctx.author.id)
        if setting is None:
            at_me_value = f'You will {"not" * (not leveling_member.settings.at_me)} be @\' when you level up.'
            embed.add_field(name='| @me', value=f'**{leveling_member.settings.at_me}**\n{at_me_value}', inline=False)

            rep_at_value = f'You will {"not" * (not leveling_member.settings.rep_at)} be messaged when your rep timer expires and you can give someone a rep point again.'
            embed.add_field(name='| rep@', value=f'**{leveling_member.settings.rep_at}**\n{rep_at_value}', inline=False)
            return await ctx.send(embed=embed)
        elif setting not in ['@me', 'rep@']:
            return await embed_maker.error(ctx, f'{setting} is not a valid setting\nValid Settings: `@me` | `rep@`')
        else:
            if setting == '@me':
                leveling_member.settings.toggle_at_me()
                if leveling_member.settings.at_me:
                    msg = 'You will now be @\'d when you level up.'
                    colour = 'green'
                else:
                    msg = 'You will no longer be @\'d when you level up'
                    colour = 'orange'

                return await embed_maker.message(ctx, description=msg, colour=colour, send=True)
            if setting == 'rep@':
                leveling_member.settings.toggle_rep_at()
                if leveling_member.settings.rep_at:
                    msg = 'I will now message you when your rep timer expires.'
                    colour = 'green'
                else:
                    msg = 'I will no longer message you when your rep timer expires.'
                    colour = 'orange'

                return await embed_maker.message(ctx, description=msg, colour=colour, send=True)

    async def process_message(self, message: discord.Message):
        author = message.author
        guild = message.guild

        leveling_member = await self.bot.leveling_system.get_member(guild.id, author.id)

        # level parliamentary route
        if not self.pp_cooldown.user_cooldown(guild.id, author.id):
            pp_add = randint(15, 25)
            await leveling_member.add_points('parliamentary', pp_add)

            branch = leveling_member.guild.get_leveling_route('parliamentary')
            current_role, levels_up, roles_up = await leveling_member.level_up(branch)

            if levels_up:
                current_role = await current_role.get_guild_role()
                await leveling_member.level_up_message(message, leveling_member.parliamentary, current_role, roles_up)

        # level honours route
        if message.channel.id in leveling_member.guild.honours_channels and not self.hp_cooldown.user_cooldown(guild.id, author.id):
            hp_add = randint(7, 12)
            await leveling_member.add_points('honours', hp_add)

            branch = leveling_member.guild.get_leveling_route('honours')
            current_role, levels_up, roles_up = await leveling_member.level_up(branch)

            if levels_up:
                current_role = await current_role.get_guild_role()
                await leveling_member.level_up_message(message, leveling_member.honours, current_role, roles_up)