Exemple #1
0
    class MyCog(commands.Cog):
        first = app_commands.Group(name='test', description='Test 1')
        second = app_commands.Group(name='test2', parent=first, description='Test 2')

        @first.command(name='cmd')
        async def test_cmd(self, interaction: discord.Interaction) -> None:
            ...

        @second.command(name='cmd2')
        async def test2_cmd(self, interaction: discord.Interaction) -> None:
            ...
Exemple #2
0
    def __init__(self,
                 *args: Any,
                 fallback: Optional[str] = None,
                 **attrs: Any) -> None:
        super().__init__(*args, **attrs)
        self.invoke_without_command = True
        parent = None
        if self.parent is not None:
            if isinstance(self.parent, HybridGroup):
                parent = self.parent.app_command
            else:
                raise TypeError(
                    f'HybridGroup parent must be HybridGroup not {self.parent.__class__}'
                )

        guild_ids = attrs.pop('guild_ids', None) or getattr(
            self.callback, '__discord_app_commands_default_guilds__', None)
        self.app_command: app_commands.Group = app_commands.Group(
            name=self.name,
            description=self.description or self.short_doc or '…',
            guild_ids=guild_ids,
        )

        # This prevents the group from re-adding the command at __init__
        self.app_command.parent = parent
        self.fallback: Optional[str] = fallback

        if fallback is not None:
            command = HybridAppCommand(self)
            command.name = fallback
            self.app_command.add_command(command)
Exemple #3
0
    class MyGroup(app_commands.Group, name='mygroup'):
        sub_group = app_commands.Group(name='mysubgroup',
                                       description='My sub-group')

        @sub_group.command()
        async def my_command(self, interaction: discord.Interaction) -> None:
            ...
Exemple #4
0
def test_descriptions_group_no_args():
    my_group = app_commands.Group(name='mygroup', description='My group')

    @my_group.command()
    async def my_command(interaction: discord.Interaction) -> None:
        """Test slash command"""

    assert my_command.description == 'Test slash command'
Exemple #5
0
def test_group_with_commands():
    my_group = app_commands.Group(name='mygroup', description='My group')

    @my_group.command()
    async def my_command(interaction: discord.Interaction) -> None:
        ...

    assert my_command.binding is None
    assert my_command.parent is my_group
    assert my_group.commands[0] is my_command
Exemple #6
0
    def __init__(self,
                 *args: Any,
                 fallback: Optional[str] = None,
                 **attrs: Any) -> None:
        super().__init__(*args, **attrs)
        self.invoke_without_command = True
        self.with_app_command: bool = attrs.pop('with_app_command', True)

        parent = None
        if self.parent is not None:
            if isinstance(self.parent, HybridGroup):
                parent = self.parent.app_command
            else:
                raise TypeError(
                    f'HybridGroup parent must be HybridGroup not {self.parent.__class__}'
                )

        # I would love for this to be Optional[app_commands.Group]
        # However, Python does not have conditional typing so it's very hard to
        # make this type depend on the with_app_command bool without a lot of needless repetition
        self.app_command: app_commands.Group = MISSING
        self.fallback: Optional[str] = fallback

        if self.with_app_command:
            guild_ids = attrs.pop('guild_ids', None) or getattr(
                self.callback, '__discord_app_commands_default_guilds__', None)
            guild_only = getattr(self.callback,
                                 '__discord_app_commands_guild_only__', False)
            default_permissions = getattr(
                self.callback, '__discord_app_commands_default_permissions__',
                None)
            nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__',
                           False)
            self.app_command = app_commands.Group(
                name=self.name,
                description=self.description or self.short_doc or '…',
                guild_ids=guild_ids,
                guild_only=guild_only,
                default_permissions=default_permissions,
                nsfw=nsfw,
            )

            # This prevents the group from re-adding the command at __init__
            self.app_command.parent = parent

            if fallback is not None:
                command = HybridAppCommand(self)
                command.name = fallback
                self.app_command.add_command(command)
Exemple #7
0
def test_descriptions_group_args():
    my_group = app_commands.Group(name='mygroup', description='My group')

    @my_group.command()
    async def my_command(interaction: discord.Interaction, arg: str,
                         arg2: int) -> None:
        """Test slash command

        Parameters
        ----------
        arg: str
            Description of arg.
            This is the second line of the arg description.
        arg2: int
            Description of arg2.
        """

    assert my_command.description == 'Test slash command'
    assert my_command._params[
        'arg'].description == 'Description of arg. This is the second line of the arg description.'
    assert my_command._params['arg2'].description == 'Description of arg2.'
Exemple #8
0
    class MyCog(commands.Cog):
        my_group = app_commands.Group(name='mygroup', description='My group')

        @my_group.command()
        async def my_command(self, interaction: discord.Interaction) -> None:
            ...
Exemple #9
0
class GuildSync:
    # The good ol switcheroo

    guildsync_group = app_commands.Group(
        name="guildsync",
        description="Sync your in-game guild roster with server roles",
        guild_only=True)

    class SyncGuild:
        def __init__(self, cog, doc, guild) -> None:
            self.doc_id = doc["_id"]
            self.guild = guild
            self.cog = cog
            self.ranks_to_role_ids = doc["rank_roles"]
            self.roles = {}
            # Reverse hashmap for performance
            self.role_ids_to_ranks = {}
            self.id = doc["gid"]
            self.key = doc["key"]
            self.tag_role_id = doc["tag_role"]
            self.tag_role = None
            self.tag_enabled = doc["enabled"]["tag"]
            self.guild_name = f"{doc['name']} [{doc['tag']}]"
            self.ranks_enabled = doc["enabled"]["ranks"]
            self.base_ep = f"guild/{self.id}/"
            self.ranks = None
            self.members = None
            self.error = None
            self.last_error = doc.get("error")
            self.create_roles = guild.me.guild_permissions.manage_roles
            self.delete_roles = self.create_roles

        async def fetch_members(self):
            ep = self.base_ep + "members"
            results = await self.cog.call_api(endpoint=ep, key=self.key)
            self.members = {r["name"]: r["rank"] for r in results}

        async def save(self,
                       *,
                       ranks=False,
                       tag_role=False,
                       edited=False,
                       error=False):
            update = {}
            if ranks:
                roles = {rank: role.id for rank, role in self.roles.items()}
                update["rank_roles"] = roles
            if tag_role:
                update["tag_role"] = self.tag_role_id
            if edited:
                update["key"] = self.key
                update["enabled.ranks"] = self.ranks_enabled
                update["enabled.tag"] = self.tag_enabled
            if error:
                update["error"] = self.error
            await self.cog.db.guildsyncs.update_one({"_id": self.doc_id},
                                                    {"$set": update})

        async def delete(self):
            await self.cog.db.guildsyncs.delete_one({"_id": self.doc_id})
            for role_id in self.ranks_to_role_ids.values():
                await self.safe_role_delete(self.guild.get_role(role_id))
            await self.safe_role_delete(self.guild.get_role(self.tag_role_id))

        async def create_role(self, rank=None):
            if not self.create_roles:
                return
            if rank:
                name = rank
            else:
                name = self.guild_name
            coro = self.guild.create_role(name=name,
                                          reason="$guildsync",
                                          color=discord.Color(
                                              self.cog.embed_color))
            try:
                return await asyncio.wait_for(coro, timeout=5)
            except discord.Forbidden:
                self.error = "Bot lacks permission to create roles."
                self.create_roles = False
            except asyncio.TimeoutError:
                self.create_roles = False
            except discord.HTTPException:
                pass

        async def safe_role_delete(self, role):
            if not self.delete_roles:
                return
            if role:
                try:
                    coro = role.delete(reason="$guildsync - role removed "
                                       "or renamed in-game")
                    await asyncio.wait_for(coro, timeout=5)
                    return True
                except (discord.Forbidden, asyncio.TimeoutError):
                    self.delete_roles = False
                except discord.HTTPException:
                    pass
                return False
            return True

        async def synchronize_roles(self):
            if self.tag_enabled:
                self.tag_role = self.guild.get_role(self.tag_role_id)
                if not self.tag_role:
                    role = await self.create_role()
                    if role:
                        self.tag_role = role
                        self.tag_role_id = role.id
                        await self.save(tag_role=True)
            else:
                if self.tag_role_id:
                    role = self.guild.get_role(self.tag_role_id)
                    if await self.safe_role_delete(role):
                        self.tag_role = None
                        self.tag_role_id = None
                        await self.save(tag_role=True)
            if self.ranks_enabled:
                ep = f"guild/{self.id}/ranks"
                try:
                    ranks = await self.cog.call_api(ep, key=self.key)
                except APIForbidden:
                    self.error = "Key has in-game leader permissions"
                    return
                except APIInvalidKey:
                    self.error = "Invalid key. Most likely deleted"
                    return
                except APIError:
                    self.error = "API error"
                    return
                self.ranks = {r["id"]: r["order"] for r in ranks}
                changed = False
                for rank in self.ranks:
                    if rank in self.ranks_to_role_ids:
                        role = self.guild.get_role(
                            self.ranks_to_role_ids[rank])
                        if role:
                            self.roles[rank] = role
                            continue
                    role = await self.create_role(rank=rank)
                    if role:
                        self.roles[rank] = role
                        changed = True
                orphaned = self.ranks_to_role_ids.keys() - self.roles
                for orphan in orphaned:
                    changed = True
                    role = self.guild.get_role(self.ranks_to_role_ids[orphan])
                    await self.safe_role_delete(role)
                self.role_ids_to_ranks = {
                    r.id: k
                    for k, r in self.roles.items()
                }
                if changed:
                    await self.save(ranks=True)

            else:
                for role_id in self.ranks_to_role_ids.values():
                    role = self.guild.get_role(role_id)
                    await self.safe_role_delete(role)
            if self.last_error and not self.error:
                self.error = None
                await self.save(error=True)

    class SyncTarget:
        @classmethod
        async def create(cls, cog, member) -> GuildSync.SyncTarget:
            self = cls()
            self.member = member
            doc = await cog.bot.database.get(member, cog)
            keys = doc.get("keys", [])
            if not keys:
                key = doc.get("key")
                if key:
                    keys.append(key)
            self.accounts = {key["account_name"] for key in keys}
            self.is_in_any_guild = False
            return self

        async def add_roles(self, roles):
            try:
                coro = self.member.add_roles(*roles, reason="$guildsync")
                await asyncio.wait_for(coro, timeout=5)
            except (asyncio.TimeoutError, discord.Forbidden):
                pass

        async def remove_roles(self, roles):
            try:
                coro = self.member.remove_roles(*roles, reason="$guildsync")
                await asyncio.wait_for(coro, timeout=5)
            except (asyncio.TimeoutError, discord.Forbidden):
                pass

        async def sync_membership(self, sync_guild: GuildSync.SyncGuild):
            lowest_order = float("inf")
            highest_rank = None
            to_add = []
            belongs = False
            current_rank_roles = {}
            current_tag_role = None
            for role in self.member.roles:
                if role.id in sync_guild.role_ids_to_ranks:
                    rank = sync_guild.role_ids_to_ranks[role.id]
                    current_rank_roles[rank] = role
                elif role.id == sync_guild.tag_role_id:
                    current_tag_role = role
            for account in self.accounts:
                if account in sync_guild.members:
                    belongs = True
                    self.is_in_any_guild = True
                    if not sync_guild.ranks_enabled:
                        break
                    if sync_guild.ranks_enabled:
                        rank = sync_guild.members[account]
                        order = sync_guild.ranks.get(rank)
                        if order:
                            if order < lowest_order:
                                lowest_order = order
                                highest_rank = rank
            if sync_guild.ranks_enabled and highest_rank:
                if highest_rank not in current_rank_roles:
                    role = sync_guild.roles.get(highest_rank)
                    if role:
                        if role < self.member.guild.me.top_role:
                            to_add.append(role)
            if sync_guild.tag_enabled and sync_guild.tag_role:
                if not current_tag_role and belongs:
                    to_add.append(sync_guild.tag_role)
            if to_add:
                await self.add_roles(to_add)
            to_remove = []
            for rank in current_rank_roles:
                if rank != highest_rank:
                    to_remove.append(current_rank_roles[rank])
            if not belongs and current_tag_role:
                to_remove.append(current_tag_role)
            if to_remove:
                await self.remove_roles(to_remove)

    async def guildsync_autocomplete(self, interaction: discord.Interaction,
                                     current: str):

        syncs = await self.db.guildsyncs.find({
            "guild_id": interaction.guild.id,
            "name": prepare_search(current)
        }).to_list(None)
        syncs = [self.SyncGuild(self, doc, interaction.guild) for doc in syncs]
        options = []
        for sync in syncs:
            options.append(Choice(name=sync.guild_name, value=sync.id))
        return options

    @guildsync_group.command(name="diagnose")
    @app_commands.checks.has_permissions(manage_guild=True, manage_roles=True)
    @app_commands.describe(sync="The guildsync to debug",
                           user="******")
    @app_commands.autocomplete(sync=guildsync_autocomplete)
    async def guildsync_diagnose(self,
                                 interaction: discord.Interaction,
                                 sync: str,
                                 user: discord.User = None):
        """Diagnose common issues with your guildsync configuration."""
        await interaction.response.defer()
        sync = await self.db.guildsyncs.find_one({
            "guild_id":
            interaction.guild.id,
            "gid":
            sync
        })
        if not sync:
            return await interaction.followup.send("No valid sync chosen")
        if not interaction.guild.me.guild_permissions.manage_roles:
            return await interaction.followup.send(
                "I don't have the manage roles permission")
        gs = self.SyncGuild(self, sync, interaction.guild)
        key_ok = await self.verify_leader_permissions(gs.key, gs.id)
        if not key_ok:
            return await interaction.followup.send(
                "The added key is no longer valid. "
                "Please change the key using /guildsync edit.")
        if not gs.ranks_enabled and not gs.tag_enabled:
            return await interaction.followup.send(
                "Both rank sync and tag role sync are disabled")
        await gs.synchronize_roles()
        if user:
            await gs.fetch_members()
            user_doc = await self.bot.database.get(user, self)
            keys = user_doc.get("keys", [])
            if not keys:
                key = user_doc.get("key")
                if key:
                    keys.append(key)
            for key in keys:
                if key["account_name"] in gs.members:
                    break
                else:
                    return await interaction.followup.send(
                        "The provided user "
                        "is not in the guild.")
        for rank in gs.roles:
            if gs.roles[rank] > interaction.guild.me.top_role:
                return await interaction.followup.send(
                    f"The synced role {gs.roles[rank]} is higher on the "
                    "hierarchy than my highest role. I am unable to sync it.")
        return await interaction.followup.send(
            "Everything seems to be "
            "in order! If you're still having trouble, ask for "
            "help in the support server.")

    @guildsync_group.command(name="edit")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.describe(
        api_key="The API key to use for authorization. Use only if you've "
        "selected the 'Change API key' operation.",
        operation="Select the operation.",
        sync="The guildsync to modify")
    @app_commands.autocomplete(sync=guildsync_autocomplete)
    @app_commands.choices(operation=[
        Choice(name="Toggle syncing ranks.  If disabled, this will delete "
               "the roles created by the bot.",
               value="ranks"),
        Choice(value="guild_role",
               name="Toggle guild role. If disabled, this will delete "
               "the roles created by the bot."),
        Choice(name="Change API key. Make sure to "
               "fill out the api_key optional argument",
               value="change_key"),
        Choice(name="Delete the guildsync", value="delete")
    ])
    async def guildsync_edit(self,
                             interaction: discord.Interaction,
                             sync: str,
                             operation: str,
                             api_key: str = None):
        """Change settings and delete guildsyncs."""
        def bool_to_on(setting):
            if setting:
                return "**ON**"
            return "**OFF**"

        await interaction.response.defer(ephemeral=True)
        if operation == "api_key" and not api_key:
            return await interaction.response.send_message(
                "You must fill the API key argument to use this operation.",
                ephemeral=True)
        sync = await self.db.guildsyncs.find_one({
            "guild_id":
            interaction.guild.id,
            "gid":
            sync
        })
        if not sync:
            return await interaction.followup.send("No valid sync chosen")
        sync = self.SyncGuild(self, sync, interaction.guild)
        embed = discord.Embed(title="Guildsync", color=self.embed_color)

        if operation == "ranks":
            sync.ranks_enabled = not sync.ranks_enabled
        elif operation == "guild_role":
            sync.tag_enabled = not sync.tag_enabled
        elif operation == "change_key":
            verified = await self.verify_leader_permissions(api_key, sync.id)
            if not verified:
                return await interaction.followup.send(
                    "The API key you provided is invalid.", ephemeral=True)
            sync.key = api_key
        elif operation == "delete":
            await sync.delete()
            return await interaction.followup.send(
                content="Sync successfully deleted")
        await sync.save(edited=True)
        lines = [
            f"Syncing ranks: {bool_to_on(sync.ranks_enabled)}",
            f"Guild role for members: {bool_to_on(sync.tag_enabled)}"
        ]
        description = "\n".join(lines)
        if sync.last_error:
            description += f"\n❗ERROR: {sync.last_error}"
        embed.description = description
        embed = discord.Embed(title=f"Post-update {sync.guild_name} settings",
                              color=self.embed_color,
                              description=description)
        await interaction.followup.send(embed=embed)
        await self.run_guildsyncs(interaction.guild)

    @guildsync_group.command(name="add")
    @app_commands.guild_only()
    @app_commands.checks.has_permissions(manage_guild=True, manage_roles=True)
    @app_commands.checks.bot_has_permissions(manage_roles=True)
    @app_commands.choices(authentication_method=[
        Choice(name="Use your own currently active API key. "
               "You need to be the guild leader",
               value="use_key"),
        Choice(name="Have the bot prompt another user for "
               "authorization. If selected, fill out user_to_prompt argument",
               value="prompt_user"),
        Choice(name="Enter a key. If selected, fill out the api_key argument",
               value="enter_key")
    ],
                          sync_type=[
                              Choice(name="Sync only the in-game ranks",
                                     value="ranks"),
                              Choice(name="Give every member of your guild a "
                                     "single, guild specific role.",
                                     value="guild_role"),
                              Choice(
                                  name="Sync both the ranks, and give every "
                                  "member a guild specific role",
                                  value="ranks_and_role")
                          ])
    @app_commands.describe(
        guild_name="The guild name of the guild "
        "you wish to sync with.",
        authentication_method="Select how you want to authenticate "
        "the leadership of the guild",
        sync_type="Select how you want the synced roles to behave.",
        user_to_prompt="The user to prompt for authorization. Use "
        "only if you've selected it as the authentication_method.",
        api_key="The api key to use for authorization. "
        "Use only if you've selected it as the authentication_method.")
    async def guildsync_add(self,
                            interaction: discord.Interaction,
                            guild_name: str,
                            sync_type: str,
                            authentication_method: str,
                            user_to_prompt: discord.User = None,
                            api_key: str = None):
        """Sync your in-game guild ranks with Discord. Add a guild."""
        if authentication_method == "prompt_user" and not user_to_prompt:
            return await interaction.response.send_message(
                "You must specify a user to prompt for authorization",
                ephemeral=True)
        if authentication_method == "enter_key" and not api_key:
            return await interaction.response.send_message(
                "You must specify an API key to use for authorization",
                ephemeral=True)
        endpoint_id = "guild/search?name=" + guild_name.title().replace(
            ' ', '%20')
        await interaction.response.defer(ephemeral=True)
        try:
            guild_ids = await self.call_api(endpoint_id)
            guild_id = guild_ids[0]
            base_ep = f"guild/{guild_id}"
            info = await self.call_api(base_ep)
        except (IndexError, APINotFound):
            return await interaction.followup.send("Invalid guild name")
        if authentication_method == "enter_key":
            if not await self.verify_leader_permissions(api_key, guild_id):
                return await interaction.followup.send(
                    "The provided key is invalid or is missing permissions")
            key = api_key
        if authentication_method == "use_key":
            try:
                await self.call_api(base_ep + "/members", interaction.user,
                                    ["guilds"])
                key_doc = await self.fetch_key(interaction.user, ["guilds"])
                key = key_doc["key"]
            except (APIForbidden, APIKeyError):
                return await interaction.followup.send(
                    "You are not the guild leader.")
        can_add = await self.can_add_sync(interaction.guild, guild_id)
        if not can_add:
            return await interaction.followup.send(
                "Cannot add this guildsync! You're either syncing with "
                "this guild already, or you've hit the limit of "
                f"{GUILDSYNC_LIMIT} active syncs. "
                f"Check `/guildsync edit`",
                ephemeral=True)
        enabled = {"tag": False, "ranks": False}
        if sync_type == "ranks_and_role":
            enabled["tag"] = True
            enabled["ranks"] = True
        elif sync_type == "ranks":
            enabled["ranks"] = True
        else:
            enabled["tag"] = True
        guild_info = {
            "enabled": enabled,
            "name": info["name"],
            "tag": info["tag"]
        }
        if authentication_method == "prompt_user":
            try:
                key_doc = await self.fetch_key(user_to_prompt, ["guilds"])
            except APIKeyError:
                return await interaction.followup.send(
                    "The user does not have a valid key")
            key = key_doc["key"]
            try:
                await self.call_api(base_ep + "/members", key=key)
            except APIForbidden:
                return await interaction.followup.send(
                    "The user is missing leader permissions.")
            try:
                embed = discord.Embed(title="Guildsync request",
                                      color=self.embed_color)
                embed.description = (
                    "User {0.name}#{0.discriminator} (`{0.id}`), an "
                    "Administrator in {0.guild.name} server (`{0.guild.id}`) "
                    "is requesting your authorization in order to enable "
                    "Guildsync for the `{1}` guild, of which you are a leader "
                    "of.\nShould you agree, the bot will use your the API key "
                    "that you currently have active to enable Guildsync. If "
                    "the key is deleted at any point the sync will stop "
                    "working.\n**Your key will never be visible to the "
                    "requesting user or anyone else**".format(
                        interaction.user, info["name"]))
                embed.set_footer(
                    text="Use the reactions below to answer. "
                    "If no response is given within three days this request "
                    "will expire.")
                view = GuildSyncPromptUserConfirmView(self)
                msg = await user_to_prompt.send(embed=embed, view=view)
                prompt_doc = {
                    "guildsync_id": guild_id,
                    "guild_id": interaction.guild.id,
                    "requester_id": interaction.user.id,
                    "created_at": datetime.utcnow(),
                    "message_id": msg.id,
                    "options": guild_info
                }
                await self.db.guildsync_prompts.insert_one(prompt_doc)
                await self.db.guildsync_prompts.create_index(
                    "created_at", expireAfterSeconds=259200)
                return await interaction.followup.send(
                    "Message successfully sent. You will be "
                    "notified when the user replies.")
            except discord.HTTPException:
                return await interaction.followup.send(
                    "Could not send a message to user - "
                    "they most likely have DMs disabled")
        await self.guildsync_success(interaction.guild,
                                     guild_id,
                                     destination=interaction.followup,
                                     key=key,
                                     options=guild_info)

    async def can_add_sync(self, guild, in_game_guild_id):
        result = await self.db.guildsyncs.find_one({
            "guild_id": guild.id,
            "gid": in_game_guild_id
        })
        if result:
            return False
        count = await self.db.guildsyncs.count_documents(
            {"guild_id": guild.id})
        if count > GUILDSYNC_LIMIT:
            return False
        return True

    async def guildsync_success(self,
                                guild,
                                in_game_guild_id,
                                *,
                                destination,
                                key,
                                options,
                                extra_message=None):
        if extra_message:
            await destination.send(extra_message)
        doc = {
            "guild_id": guild.id,
            "gid": in_game_guild_id,
            "key": key,
            "rank_roles": {},
            "tag_role": None
        } | options
        can_add = await self.can_add_sync(guild, in_game_guild_id)
        if not can_add:
            return await destination.send(
                "Cannot add guildsync. You've either reached the limit "
                f"of {GUILDSYNC_LIMIT} active syncs, or you're trying to add "
                "one that already exists. See $guildsync edit.")
        await self.db.guildsyncs.insert_one(doc)
        guild_doc = await self.bot.database.get(guild, self)
        sync_doc = guild_doc.get("guildsync", {})
        enabled = sync_doc.get("enabled", None)
        if enabled is None:
            await self.bot.database.set(guild, {"guildsync.enabled": True},
                                        self)
        await destination.send("Guildsync succesfully added!")
        await self.run_guildsyncs(guild)

    @guildsync_group.command(name="toggle")
    @app_commands.guild_only()
    @app_commands.describe(
        enabled="Enable or disable guildsync for this server")
    @app_commands.checks.has_permissions(manage_guild=True, manage_roles=True)
    async def sync_toggle(self, interaction: discord.Interaction,
                          enabled: bool):
        """Global toggle for guildsync - does not wipe the settings"""
        guild = interaction.guild
        await self.bot.database.set_guild(guild,
                                          {"guildsync.enabled": enabled}, self)
        if enabled:
            msg = ("Guildsync is now enabled. You may still need to "
                   "add guildsyncs using `guildsync add` before it "
                   "is functional.")
        else:
            msg = ("Guildsync is now disabled globally on this server. "
                   "Run this command again to enable it.")
        await interaction.response.send_message(msg)

    async def guildsync_now(self, ctx):
        """Force a synchronization"""
        await self.run_guildsyncs(ctx.guild)

    @guildsync_group.command(name="purge")
    @app_commands.guild_only()
    @app_commands.describe(
        enabled="Enable or disable purge. You'll be asked to confirm your "
        "selection afterwards.")
    @app_commands.checks.has_permissions(manage_guild=True,
                                         manage_roles=True,
                                         kick_members=True)
    @app_commands.checks.bot_has_permissions(kick_members=True,
                                             manage_roles=True)
    async def sync_purge(self, interaction: discord.Interaction,
                         enabled: bool):
        """Toggle kicking of users that are not in any of the synced guilds."""
        if enabled:
            view = ConfirmPurgeView()
            await interaction.response.send_message(
                "Members without any other role that have been in the "
                "server for longer than 48 hours will be kicked during guild "
                "syncs.",
                view=view,
                ephemeral=True)
            await view.wait()
            if view.value is None:
                return await interaction.edit_original_message(
                    content="Timed out.", view=None)
            elif view.value:
                await self.bot.database.set(interaction.guild,
                                            {"guildsync.purge": enabled}, self)
        else:
            await self.bot.database.set(interaction.guild,
                                        {"guildsync.purge": enabled}, self)
            await interaction.response.send_message("Disabled purge.")

    async def verify_leader_permissions(self, key, guild_id):
        try:
            await self.call_api(f"guild/{guild_id}/members", key=key)
            return True
        except APIError:
            return False

    async def run_guildsyncs(self, guild, *, sync_for=None):
        guild_doc = await self.bot.database.get(guild, self)
        guildsync_doc = guild_doc.get("guildsync", {})
        enabled = guildsync_doc.get("enabled", False)
        if not enabled:
            return
        purge = guildsync_doc.get("purge", False)
        cursor = self.db.guildsyncs.find({"guild_id": guild.id})
        targets = []
        if sync_for:
            target = targets.append(await
                                    self.SyncTarget.create(self, sync_for))
        else:
            for member in guild.members:
                target = await self.SyncTarget.create(self, member)
                if target:
                    targets.append(target)
        async for doc in cursor:
            try:
                sync = self.SyncGuild(self, doc, guild)
                await sync.synchronize_roles()
                if sync.error:
                    await sync.save(error=True)
                    if sync.ranks_enabled and not sync.roles:
                        continue
                try:
                    await sync.fetch_members()
                except APIError:
                    sync.error = "Couldn't fetch guild members."
                    print("failed")
                    await sync.save(error=True)
                    continue
                for target in targets:
                    await target.sync_membership(sync)
            except Exception as e:
                self.log.exception("Exception in guildsync", exc_info=e)
        if purge:
            for target in targets:
                member = target.member
                membership_duration = (datetime.utcnow() -
                                       member.joined_at).total_seconds()
                if not target.is_in_any_guild:
                    if len(member.roles) == 1 and membership_duration > 172800:
                        try:
                            await member.guild.kick(user=member,
                                                    reason="$guildsync purge")
                        except discord.Forbidden:
                            pass

    @tasks.loop(seconds=60)
    async def guildsync_consumer(self):
        while True:
            _, coro = await self.guildsync_queue.get()
            await asyncio.wait_for(coro, timeout=300)
            self.guildsync_queue.task_done()
            await asyncio.sleep(0.5)

    @guildsync_consumer.before_loop
    async def before_guildsync_consumer(self):
        await self.bot.wait_until_ready()

    @tasks.loop(seconds=60)
    async def guild_synchronizer(self):
        cursor = self.bot.database.iter("guilds", {"guildsync.enabled": True},
                                        self,
                                        batch_size=10)
        async for doc in cursor:
            try:
                if doc["_obj"]:
                    coro = self.run_guildsyncs(doc["_obj"])
                    await asyncio.wait_for(coro, timeout=200)
            except asyncio.CancelledError:
                return
            except Exception:
                pass

    @guild_synchronizer.before_loop
    async def before_guild_synchronizer(self):
        await self.bot.wait_until_ready()

    def schedule_guildsync(self, guild, priority, *, member=None):
        coro = self.run_guildsyncs(guild, sync_for=member)
        self.guildsync_entry_number += 1
        self.guildsync_queue.put_nowait(
            ((priority, self.guildsync_entry_number), coro))

    @commands.Cog.listener("on_member_join")
    async def guildsync_on_member_join(self, member):
        if member.bot:
            return
        guild = member.guild
        doc = await self.bot.database.get(guild, self)
        sync = doc.get("guildsync", {})
        enabled = sync.get("enabled", False)
        if enabled:
            await self.run_guildsyncs(guild, sync_for=member)
Exemple #10
0
class WvwMixin:

    wvw_group = app_commands.Group(name="wvw",
                                   description="WvW related commands")

    async def world_autocomplete(self, interaction: discord.Interaction,
                                 current: str):
        if not current:
            return []
        current = current.lower()
        query = prepare_search(current)
        query = {"name": query}
        items = await self.db.worlds.find(query).to_list(25)
        return [Choice(name=it["name"], value=str(it["_id"])) for it in items]

    @wvw_group.command(name="info")
    @app_commands.describe(
        world="World name. Leave blank to use your account's world")
    @app_commands.autocomplete(world=world_autocomplete)
    async def wvw_info(self,
                       interaction: discord.Interaction,
                       *,
                       world: str = None):
        """Info about a world. Defaults to account's world"""
        user = interaction.user
        await interaction.response.defer()
        if not world:
            endpoint = "account"
            results = await self.call_api(endpoint, user)
            wid = results["world"]
        else:
            wid = world
        if not wid:
            return await interaction.followup.send("Invalid world name")
        endpoints = [
            "wvw/matches?world={0}".format(wid), "worlds?id={0}".format(wid)
        ]
        matches, worldinfo = await self.call_multiple(endpoints)
        linked_worlds = []
        worldcolor = "green"
        for key, value in matches["all_worlds"].items():
            if wid in value:
                worldcolor = key
                value.remove(wid)
                linked_worlds = value
                break
        if worldcolor == "red":
            color = discord.Colour.red()
        elif worldcolor == "green":
            color = discord.Colour.green()
        else:
            color = discord.Colour.blue()
        linked_worlds = [await self.get_world_name(w) for w in linked_worlds]
        score = matches["scores"][worldcolor]
        ppt = 0
        victoryp = matches["victory_points"][worldcolor]
        for m in matches["maps"]:
            for objective in m["objectives"]:
                if objective["owner"].lower() == worldcolor:
                    ppt += objective["points_tick"]
        population = worldinfo["population"]
        if population == "VeryHigh":
            population = "Very high"
        kills = matches["kills"][worldcolor]
        deaths = matches["deaths"][worldcolor]
        kd = round((kills / deaths), 2)
        data = discord.Embed(description="Performance", colour=color)
        data.add_field(name="Score", value=score)
        data.add_field(name="Points per tick", value=ppt)
        data.add_field(name="Victory Points", value=victoryp)
        data.add_field(name="K/D ratio", value=str(kd), inline=False)
        data.add_field(name="Population", value=population)
        if linked_worlds:
            data.add_field(name="Linked with", value=", ".join(linked_worlds))
        data.set_author(name=worldinfo["name"])
        if MATPLOTLIB_AVAILABLE:
            graph = await self.get_population_graph(worldinfo)
            data.set_image(url=f"attachment://{graph.filename}")
            return await interaction.followup.send(embed=data, file=graph)
        await interaction.followup.send(embed=data)

    @wvw_group.command(name="population_track")
    @app_commands.describe(
        world="Specify the name of a World to track the population of, and "
        "recieve a notification when the specified World is no longer full")
    @app_commands.autocomplete(world=world_autocomplete)
    async def wvw_population_track(self, interaction: discord.Interaction,
                                   world: str):
        """Receive a notification when the world is no longer full"""
        user = interaction.user
        await interaction.response.defer(ephemeral=True)
        wid = world
        if not wid:
            return await interaction.followup.send("Invalid world name")
        doc = await self.bot.database.get_user(user, self)
        if doc and wid in doc.get("poptrack", []):
            return await interaction.followup.send(
                "You're already tracking this world")
        results = await self.call_api("worlds/{}".format(wid))
        if results["population"] != "Full":
            return await interaction.followup.send(
                "This world is currently not full!")
        await interaction.followup.send(
            "You will be notiifed when {} is no longer full "
            "".format(world.title()))
        await self.bot.database.set(user, {"poptrack": wid},
                                    self,
                                    operator="$push")

    def population_to_int(self, pop):
        pops = ["low", "medium", "high", "veryhigh", "full"]
        return pops.index(pop.lower().replace("_", ""))

    async def get_population_graph(self, world):
        cursor = self.db.worldpopulation.find({"world_id": world["id"]})
        data = []
        async for doc in cursor:
            data.append((doc["date"], doc["population"]))
        data.append((datetime.datetime.utcnow(),
                     self.population_to_int(world["population"])))
        data.sort(key=lambda x: x[0])
        graph = await self.bot.loop.run_in_executor(None,
                                                    generate_population_graph,
                                                    data)
        file = discord.File(graph, "graph.png")
        return file
Exemple #11
0
class GuildManageMixin:

    server_group = app_commands.Group(name="server",
                                      description="Server management commands",
                                      guild_only=True)

    @server_group.command(name="force_account_names")
    @app_commands.checks.has_permissions(manage_nicknames=True)
    @app_commands.checks.bot_has_permissions(manage_nicknames=True)
    @app_commands.describe(
        enabled="Enable or disable automatically changing user "
        "nicknames to match in-game account name")
    async def server_force_account_names(self,
                                         interaction: discord.Interaction,
                                         enabled: bool):
        """Automatically change all server member nicknames to in-game names"""
        guild = interaction.guild
        doc = await self.bot.database.get(guild, self)
        if doc and enabled and doc.get("forced_account_names"):
            return await interaction.response.send_message(
                "Forced account names are already enabled")
        if not enabled:
            await self.bot.database.set(guild, {"force_account_names": False},
                                        self)
            return await interaction.response.send_message(
                "Forced account names disabled")
        await self.bot.database.set(guild, {"force_account_names": True}, self)
        await self.force_guild_account_names(guild)
        await interaction.response.send_message(
            content="Automatic account names enabled. To disable, use "
            "`/server forceaccountnames false`\nPlease note that the "
            "bot cannot change nicknames for roles above the bot.")

    @server_group.command(name="preview_chat_links")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.describe(
        enabled="Enable or disable automatic chat link preview")
    async def previewchatlinks(self, interaction: discord.Interaction,
                               enabled: bool):
        """Enable or disable automatic GW2 chat link preview"""
        guild = interaction.guild
        doc = await self.bot.database.get(interaction.guild, self)
        disabled = doc.get("link_preview_disabled", False)
        if disabled and not enabled:
            return await interaction.response.send_message(
                "Chat link preview is aleady disabled.", ephemeral=True)
        if not disabled and enabled:
            return await interaction.response.send_message(
                "Chat link preview is aleady enabled.", ephemeral=True)
        if not disabled and not enabled:
            self.chatcode_preview_opted_out_guilds.add(guild.id)
            return await interaction.response.send_message(
                "Chat link preview is now disabled.", ephemeral=True)
        if disabled and enabled:
            await self.bot.database.set_guild(
                guild, {"link_preview_disabled": not enabled}, self)
            await self.bot.database.set_guild(
                guild, {"link_preview_disabled": not enabled}, self)
            try:
                self.chatcode_preview_opted_out_guilds.remove(guild.id)
            except KeyError:
                pass
            return await interaction.response.send_message(
                "Chat link preview is now enabled.", ephemeral=True)

    @server_group.command(name="sync_now")
    @app_commands.checks.has_permissions(manage_guild=True)
    async def sync_now(self, interaction: discord.Interaction):
        """Force a sync for any Guildsyncs and Worldsyncs you have"""
        await interaction.response.send_message("Syncs scheduled!")
        await self.guildsync_now(interaction)
        await self.worldsync_now(interaction)

    @server_group.command(name="api_key_role")
    @app_commands.checks.has_permissions(manage_roles=True)
    @app_commands.checks.bot_has_permissions(manage_roles=True)
    @app_commands.describe(
        enabled="Enable or disable giving members with an API key a role",
        role="The role that will be given to members with an API key added")
    async def server_key_sync(self,
                              interaction: discord.Interaction,
                              enabled: bool,
                              role: discord.Role = None):
        """A feature to automatically add a role to members that have added an
        API key to the bot."""
        if enabled and not role:
            return await interaction.response.send_message(
                "If enabling, you must specify a role "
                "to give to members with an API key.",
                ephemeral=True)
        guild = interaction.guild
        await self.bot.database.set(guild, {
            "key_sync.enabled": enabled,
            "key_sync.role": role.id
        }, self)
        if enabled:
            if not role:
                return await interaction.response.send_message(
                    "Please specify a role.")
            await interaction.response.send_message(
                "Key sync enabled. Members with valid API keys "
                "will now be given the selected role")
            return await self.key_sync_guild(guild)
        await interaction.response.send_message("Key sync disabled.")

    @tasks.loop(minutes=5)
    async def key_sync_task(self):
        cursor = self.bot.database.iter("guilds", {"key_sync.enabled": True},
                                        self)
        async for doc in cursor:
            try:
                guild = doc["_obj"]
                role = guild.get_role(doc["key_sync"]["role"])
                if not role:
                    continue
                await self.key_sync_guild(guild, role)
            except asyncio.CancelledError:
                return
            except Exception:
                pass

    async def key_sync_guild(self, guild, role=None):
        if not role:
            doc = await self.bot.database.get(guild, self)
            enabled = doc.get("key_sync", {}).get("enabled")
            if not enabled:
                return
            role = guild.get_role(doc["key_sync"]["role"])
        if not role:
            return
        doc = await self.bot.database.get(guild, self)
        role = guild.get_role(doc["key_sync"]["role"])
        if not role:
            return
        for member in guild.members:
            await self.key_sync_user(member, role)

    async def key_sync_user(self, member, role=None):
        guild = member.guild
        if not guild.me.guild_permissions.manage_roles:
            return
        if not role:
            doc = await self.bot.database.get(guild, self)
            enabled = doc.get("key_sync", {}).get("enabled")
            if not enabled:
                return
            role = guild.get_role(doc["key_sync"]["role"])
        if not role:
            return
        user_doc = await self.bot.database.get(member, self)
        has_key = False
        if user_doc.get("key", {}).get("key"):
            has_key = True
        try:
            if has_key:
                if role not in member.roles:
                    await member.add_roles(role, reason="/server api_key_role")
            else:
                if role in member.roles:
                    await member.remove_roles(
                        role,
                        reason="/server api_key_role is enabled. Member "
                        "lacks a valid API key.")
        except discord.Forbidden:
            return

    @key_sync_task.before_loop
    async def before_forced_account_names(self):
        await self.bot.wait_until_ready()

    async def force_guild_account_names(self, guild):
        for member in guild.members:
            try:
                key = await self.fetch_key(member)
                name = key["account_name"]
                if name.lower() not in member.display_name.lower():
                    await member.edit(nick=name,
                                      reason="Force account names - /server")
            except Exception:
                pass
Exemple #12
0
class AdminCog(commands.Cog):
    def __init__(self, bot: SpellBot):
        self.bot = bot

    @app_commands.command(name="setup", description="Setup SpellBot on your server.")
    @tracer.wrap(name="interaction", resource="setup")
    async def setup(self, interaction: discord.Interaction) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.setup()

    set_group = app_commands.Group(name="set", description="...")

    @set_group.command(
        name="motd",
        description="Set your server's message of the day. Leave blank to unset.",
    )
    @app_commands.describe(message="Message content")
    @tracer.wrap(name="interaction", resource="set_motd")
    async def motd(self, interaction: discord.Interaction, message: Optional[str] = None) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_motd(message)

    @set_group.command(
        name="channel_motd",
        description="Set this channel's message of the day. Leave blank to unset.",
    )
    @tracer.wrap(name="interaction", resource="set_channel_motd")
    async def channel_motd(
        self,
        interaction: discord.Interaction,
        message: Optional[str] = None,
    ) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_channel_motd(message)

    @app_commands.command(
        name="channels",
        description="Show the current configurations for channels on your server.",
    )
    @app_commands.describe(page="If there are multiple pages of output, which one?")
    @tracer.wrap(name="interaction", resource="channels")
    async def channels(self, interaction: discord.Interaction, page: Optional[int] = 1) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            assert page and page >= 1
            await action.channels(page=page)

    @app_commands.command(name="awards", description="Setup player awards on your server.")
    @app_commands.describe(page="If there are multiple pages of output, which one?")
    @tracer.wrap(name="interaction", resource="awards")
    async def awards(self, interaction: discord.Interaction, page: Optional[int] = 1) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            assert page and page >= 1
            await action.awards(page=page)

    award_group = app_commands.Group(name="award", description="...")

    @award_group.command(name="add", description="Add a new award level to the list of awards.")
    @app_commands.describe(count="The number of games needed for this award")
    @app_commands.describe(role="The role to assign when a player gets this award")
    @app_commands.describe(message="The message to send players you get this award")
    @app_commands.describe(repeating="Repeatedly give this award every X games?")
    @app_commands.describe(
        remove="Instead of assigning the role, remove it from the player",
    )
    @tracer.wrap(name="interaction", resource="award_add")
    async def award_add(
        self,
        interaction: discord.Interaction,
        count: int,
        role: discord.Role,
        message: str,
        repeating: Optional[bool] = False,
        remove: Optional[bool] = False,
    ) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.award_add(count, str(role), message, repeating=repeating, remove=remove)

    @award_group.command(
        name="delete",
        description="Delete an existing award level from the server.",
    )
    @app_commands.describe(id="The ID number of the award to delete")
    @tracer.wrap(name="interaction", resource="award_delete")
    async def award_delete(self, interaction: discord.Interaction, id: int) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.award_delete(id)

    @set_group.command(
        name="default_seats",
        description="Set the default number of seats for new games in this channel.",
    )
    @app_commands.describe(seats="Default number of seats")
    @app_commands.choices(
        seats=[
            Choice(name="2", value=2),
            Choice(name="3", value=3),
            Choice(name="4", value=4),
        ],
    )
    @tracer.wrap(name="interaction", resource="set_default_seats")
    async def default_seats(self, interaction: discord.Interaction, seats: int) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_default_seats(seats)

    @set_group.command(
        name="auto_verify",
        description="Should posting in this channel automatically verify users?",
    )
    @app_commands.describe(setting="Setting")
    @tracer.wrap(name="interaction", resource="set_auto_verify")
    async def auto_verify(self, interaction: discord.Interaction, setting: bool) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_auto_verify(setting)

    @set_group.command(
        name="verified_only",
        description="Should only verified users be allowed to post in this channel?",
    )
    @app_commands.describe(setting="Setting")
    @tracer.wrap(name="interaction", resource="set_verified_only")
    async def verified_only(self, interaction: discord.Interaction, setting: bool) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_verified_only(setting)

    @set_group.command(
        name="unverified_only",
        description="Should only unverified users be allowed to post in this channel?",
    )
    @app_commands.describe(setting="Setting")
    @tracer.wrap(name="interaction", resource="set_unverified_only")
    async def unverified_only(self, interaction: discord.Interaction, setting: bool) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_unverified_only(setting)

    @app_commands.command(name="info", description="Request a DM with full game information.")
    @app_commands.describe(game_id="SpellBot ID of the game")
    @tracer.wrap(name="interaction", resource="info")
    async def info(self, interaction: discord.Interaction, game_id: str) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.info(game_id)

    @set_group.command(
        name="voice_category",
        description="Set the voice category prefix for games in this channel.",
    )
    @app_commands.describe(prefix="Setting")
    @tracer.wrap(name="interaction", resource="set_voice_category")
    async def voice_category(self, interaction: discord.Interaction, prefix: str) -> None:
        add_span_context(interaction)
        async with AdminAction.create(self.bot, interaction) as action:
            await action.set_voice_category(prefix)
Exemple #13
0
class CommerceMixin:
    tp_group = app_commands.Group(name="tp",
                                  description="Trading post related commands")
    gem_group = app_commands.Group(name="gem",
                                   description="Gem related commands")

    @tp_group.command(name="selling")
    async def tp_selling(self, interaction: discord.Interaction):
        """Show current selling transactions"""
        await interaction.response.defer()
        embed = await self.get_tp_embed(interaction, "sells")
        if not embed:
            return
        await interaction.followup.send(embed=embed)

    @tp_group.command(name="buying")
    async def tp_buying(self, interaction: discord.Interaction):
        """Show current buying transactions"""
        await interaction.response.defer()
        embed = await self.get_tp_embed(interaction, "buys")
        if not embed:
            return
        await interaction.followup.send(embed=embed)

    async def get_tp_embed(self, interaction, state):
        endpoint = "commerce/transactions/current/" + state
        doc = await self.fetch_key(interaction.user, ["tradingpost"])
        results = await self.call_api(endpoint, key=doc["key"])
        data = discord.Embed(description='Current ' + state,
                             colour=await self.get_embed_color(interaction))
        data.set_author(name=f'Transaction overview of {doc["account_name"]}')
        data.set_thumbnail(url=("https://wiki.guildwars2.com/"
                                "images/thumb/d/df/Black-Lion-Logo.png/"
                                "300px-Black-Lion-Logo.png"))
        data.set_footer(text="Black Lion Trading Company")
        results = results[:20]  # Only display 20 most recent transactions
        item_id = ""
        dup_item = {}
        # Collect listed items
        for result in results:
            item_id += str(result["item_id"]) + ","
            if result["item_id"] not in dup_item:
                dup_item[result["item_id"]] = len(dup_item)
        # Get information about all items, doesn't matter if string ends with ,
        endpoint_listing = "commerce/listings?ids={0}".format(str(item_id))
        # Call API once for all items
        try:
            listings = await self.call_api(endpoint_listing)
        except APIBadRequest:
            await interaction.followup.send("You don't have any ongoing "
                                            "transactions")
            return None
        for result in results:
            index = dup_item[result["item_id"]]
            price = result["price"]
            itemdoc = await self.fetch_item(result["item_id"])
            quantity = result["quantity"]
            item_name = itemdoc["name"]
            offers = listings[index][state]
            max_price = offers[0]["unit_price"]
            undercuts = 0
            op = operator.lt if state == "buys" else operator.gt
            for offer in offers:
                if op(offer["unit_price"], price):
                    break
                undercuts += offer["listings"]
            undercuts = "· Undercuts: {}".format(
                undercuts) if undercuts else ""
            if quantity == 1:
                total = ""
            else:
                total = " - Total: " + self.gold_to_coins(
                    interaction, quantity * price)
            data.add_field(name=item_name,
                           value="{} x {}{}\nMax. offer: {} {}".format(
                               quantity,
                               self.gold_to_coins(interaction, price), total,
                               self.gold_to_coins(interaction,
                                                  max_price), undercuts),
                           inline=False)
        return data

    async def tp_autocomplete(self, interaction: discord.Interaction,
                              current: str):
        if not current:
            return []
        query = prepare_search(current)
        query = {
            "name": query,
            "flags": {
                "$nin": ["AccountBound", "SoulbindOnAcquire"]
            }
        }
        items = await self.db.items.find(query).to_list(25)
        items = sorted(items, key=lambda c: c["name"])
        return [Choice(name=it["name"], value=str(it["_id"])) for it in items]

    @tp_group.command(name="price")
    @app_commands.autocomplete(item=tp_autocomplete)
    @app_commands.describe(
        item="Specify the name of an item to check the price of")
    async def tp_price(self, interaction: discord.Interaction, item: str):
        """Check price of an item"""
        await interaction.response.defer()
        try:
            commerce = 'commerce/prices/'
            endpoint = commerce + item
            results = await self.call_api(endpoint)
        except APINotFound:
            return await interaction.followup.send("This item isn't on the TP."
                                                   )
        except APIError:
            raise
        choice = await self.db.items.find_one({"_id": int(item)})
        buyprice = results["buys"]["unit_price"]
        sellprice = results["sells"]["unit_price"]
        itemname = choice["name"]
        level = str(choice["level"])
        rarity = choice["rarity"]
        itemtype = self.gamedata["items"]["types"][choice["type"]].lower()
        description = "A level {} {} {}".format(level, rarity.lower(),
                                                itemtype.lower())
        if buyprice != 0:
            buyprice = self.gold_to_coins(interaction, buyprice)
        else:
            buyprice = "No buy orders"
        if sellprice != 0:
            sellprice = self.gold_to_coins(interaction, sellprice)
        else:
            sellprice = "No sell orders"
        embed = discord.Embed(title=itemname,
                              description=description,
                              colour=self.rarity_to_color(rarity))
        if "icon" in choice:
            embed.set_thumbnail(url=choice["icon"])
        embed.add_field(name="Buy price", value=buyprice, inline=False)
        embed.add_field(name="Sell price", value=sellprice, inline=False)
        embed.set_footer(text=choice["chat_link"])
        await interaction.followup.send(embed=embed)

    @tp_group.command(name="delivery")
    async def tp_delivery(self, interaction: discord.Interaction):
        """Show your items awaiting in delivery box"""
        endpoint = "commerce/delivery/"
        await interaction.response.defer()
        doc = await self.fetch_key(interaction.user, ["tradingpost"])
        results = await self.call_api(endpoint, key=doc["key"])
        data = discord.Embed(description='Current deliveries',
                             colour=await self.get_embed_color(interaction))
        data.set_author(name=f'Delivery overview of {doc["account_name"]}')
        data.set_thumbnail(url="https://wiki.guildwars2.com/"
                           "images/thumb/d/df/Black-Lion-Logo.png"
                           "/300px-Black-Lion-Logo.png")
        data.set_footer(text="Black Lion Trading Company")
        coins = results["coins"]
        items = results["items"]
        items = items[:20]  # Get only first 20 entries
        item_quantity = []
        itemlist = []
        if coins == 0:
            gold = "Currently no coins for pickup."
        else:
            gold = self.gold_to_coins(interaction, coins)
        data.add_field(name="Coins", value=gold, inline=False)
        counter = 0
        if len(items) != 0:
            for item in items:
                item_quantity.append(item["count"])
                itemdoc = await self.fetch_item(item["id"])
                itemlist.append(itemdoc)
            for item in itemlist:
                item_name = item["name"]
                # Get quantity of items
                quantity = item_quantity[counter]
                counter += 1
                data.add_field(name=item_name,
                               value="x {0}".format(quantity),
                               inline=False)
        else:
            if coins == 0:
                return await interaction.followup.send(
                    "Your delivery box is empty!")
            data.add_field(name="No current deliveries.",
                           value="Have fun!",
                           inline=False)
        await interaction.followup.send(embed=data)

    def gold_to_coins(self, ctx, money):
        gold, remainder = divmod(money, 10000)
        silver, copper = divmod(remainder, 100)
        kwargs = {"fallback": True, "fallback_fmt": " {} "}
        gold = "{}{}".format(gold, self.get_emoji(ctx, "gold", **
                                                  kwargs)) if gold else ""
        silver = "{}{}".format(silver, self.get_emoji(
            ctx, "silver", **kwargs)) if silver else ""
        copper = "{}{}".format(copper, self.get_emoji(
            ctx, "copper", **kwargs)) if copper else ""
        return "".join(filter(None, [gold, silver, copper]))

    def rarity_to_color(self, rarity):
        return int(self.gamedata["items"]["rarity_colors"][rarity], 0)

    @gem_group.command(name="price")
    @app_commands.describe(
        quantity="The number of gems to evaluate (default is 400)")
    async def gem_price(self,
                        interaction: discord.Interaction,
                        quantity: int = 400):
        """Lists current gold/gem exchange prices."""
        if quantity <= 1:
            return await interaction.followup.send(
                "Quantity must be higher than 1")
        await interaction.response.defer()
        gem_price = await self.get_gem_price(quantity)
        coin_price = await self.get_coin_price(quantity)
        data = discord.Embed(title="Currency exchange",
                             colour=await self.get_embed_color(interaction))
        data.add_field(name="{} gems would cost you".format(quantity),
                       value=self.gold_to_coins(interaction, gem_price),
                       inline=False)
        data.set_thumbnail(url="https://render.guildwars2.com/file/220061640EC"
                           "A41C0577758030357221B4ECCE62C/502065.png")
        data.add_field(name="{} gems could buy you".format(quantity),
                       value=self.gold_to_coins(interaction, coin_price),
                       inline=False)
        await interaction.followup.send(embed=data)

    async def get_gem_price(self, quantity=400):
        endpoint = "commerce/exchange/coins?quantity=10000000"
        results = await self.call_api(endpoint)
        cost = results['coins_per_gem'] * quantity
        return cost

    async def get_coin_price(self, quantity=400):
        endpoint = "commerce/exchange/gems?quantity={}".format(quantity)
        results = await self.call_api(endpoint)
        return results["quantity"]

    @gem_group.command(name="track")
    @app_commands.describe(gold="Receive a notification when price of 400 "
                           "gems drops below this amount. Set to 0 to disable")
    async def gem_track(self, interaction: discord.Interaction, gold: int):
        """Receive a notification when cost of 400 gems drops below given cost
        """
        if not 0 <= gold <= 500:
            return await interaction.response.send_message(
                "Invalid value. Gold may be between 0 and 500", ephemeral=True)
        price = gold * 10000
        await interaction.response.send_message(
            "You will be notified when price of 400 gems "
            f"drops below {gold} gold",
            ephemeral=True)
        await self.bot.database.set(interaction.user, {"gemtrack": price},
                                    self)
Exemple #14
0
class EvtcMixin:

    evtc_automation_group = app_commands.Group(
        name="evtc_automation",
        description="Character relating to automating EVTC processing")

    autopost_group = app_commands.Group(
        name="autopost",
        description="Automatically post processed EVTC logs uploaded by "
        "third party utilities",
        parent=evtc_automation_group)

    async def get_dpsreport_usertoken(self, user):
        doc = await self.bot.database.get(user, self)
        token = doc.get("dpsreport_token")
        if not token:
            try:
                async with self.session.get(TOKEN_URL) as r:
                    data = await r.json()
                    token = data["userToken"]
                    await self.bot.database.set(user,
                                                {"dpsreport_token": token},
                                                self)
                    return token
            except Exception:
                return None

    async def upload_log(self, file, user):
        params = {"json": 1}
        token = await self.get_dpsreport_usertoken(user)
        if token:
            params["userToken"] = token
        data = aiohttp.FormData()
        data.add_field("file", await file.read(), filename=file.filename)
        async with self.session.post(UPLOAD_URL, data=data,
                                     params=params) as r:
            resp = await r.json()
            error = resp["error"]
            if error:
                raise APIError(error)
            return resp

    async def find_duplicate_dps_report(self, doc):
        margin_of_error = datetime.timedelta(seconds=10)
        doc = await self.db.encounters.find_one({
            "boss_id": doc["boss_id"],
            "players": {
                "$eq": doc["players"]
            },
            "date": {
                "$gte": doc["date"] - margin_of_error,
                "$lt": doc["date"] + margin_of_error
            },
            "start_date": {
                "$gte": doc["start_date"] - margin_of_error,
                "$lt": doc["start_date"] + margin_of_error
            },
        })
        return True if doc else False

    async def get_encounter_data(self, encounter_id):
        async with self.session.get(JSON_URL, params={"id":
                                                      encounter_id}) as r:
            return await r.json()

    async def upload_embed(self, destination, data, permalink):
        force_emoji = True if not destination else False
        lines = []
        targets = data["phases"][0]["targets"]
        group_dps = 0
        wvw = data["triggerID"] == 1
        for target in targets:
            group_dps += sum(p["dpsTargets"][target][0]["dps"]
                             for p in data["players"])

        def get_graph(percentage):
            bar_count = round(percentage / 5)
            bars = ""
            bars += "▀" * bar_count
            bars += "━" * (20 - bar_count)
            return bars

        def get_dps(player):
            bars = ""
            dps = player["dps"]
            if not group_dps or not dps:
                percentage = 0
            else:
                percentage = round(100 / group_dps * dps)
            bars = get_graph(percentage)
            bars += f"` **{dps}** DPS | **{percentage}%** of group DPS"
            return bars

        players = []
        for player in data["players"]:
            dps = 0
            for target in targets:
                dps += player["dpsTargets"][target][0]["dps"]
            player["dps"] = dps
            players.append(player)
        players.sort(key=lambda p: p["dps"], reverse=True)
        for player in players:
            down_count = player["defenses"][0]["downCount"]
            prof = self.get_emoji(destination,
                                  player["profession"],
                                  force_emoji=True)
            line = f"{prof} **{player['name']}** *({player['account']})*"
            if down_count:
                line += (
                    f" | {self.get_emoji(destination, 'downed', force_emoji=True)}Downed "
                    f"count: **{down_count}**")
            lines.append(line)
        dpses = []
        charater_name_max_length = 19
        for player in players:
            line = self.get_emoji(destination,
                                  player["profession"],
                                  fallback=True,
                                  fallback_fmt="",
                                  force_emoji=True)
            align = (charater_name_max_length - len(player["name"])) * " "
            line += "`" + player["name"] + align + get_dps(player)
            dpses.append(line)
        dpses.append(f"> Group DPS: **{group_dps}**")
        color = discord.Color.green(
        ) if data["success"] else discord.Color.red()
        minutes, seconds = data["duration"].split()[:2]
        minutes = int(minutes[:-1])
        seconds = int(seconds[:-1])
        duration_time = (minutes * 60) + seconds
        duration = f"**{minutes}** minutes, **{seconds}** seconds"
        embed = discord.Embed(title="DPS Report",
                              description="Encounter duration: " + duration,
                              url=permalink,
                              color=color)
        boss_lines = []
        for target in targets:
            target = data["targets"][target]
            if data["success"]:
                health_left = 0
            else:
                percent_burned = target["healthPercentBurned"]
                health_left = 100 - percent_burned
            health_left = round(health_left, 2)
            if len(targets) > 1:
                boss_lines.append(f"**{target['name']}**")
            boss_lines.append(f"Health: **{health_left}%**")
            boss_lines.append(get_graph(health_left))
        embed.add_field(name="> **BOSS**", value="\n".join(boss_lines))
        buff_lines = []
        sought_buffs = ["Might", "Fury", "Quickness", "Alacrity", "Protection"]
        buffs = []
        for buff in sought_buffs:
            for key, value in data["buffMap"].items():
                if value["name"] == buff:
                    buffs.append({
                        "name": value["name"],
                        "id": int(key[1:]),
                        "stacking": value["stacking"]
                    })
                    break
        separator = 2 * en_space
        line = zero_width_space + (en_space * (charater_name_max_length + 6))
        icon_line = line
        blank = self.get_emoji(destination, "blank", force_emoji=True)
        first = True
        for buff in sought_buffs:
            if first and not blank:
                icon_line = icon_line[:-2]
            if not first:
                if blank:
                    icon_line += blank + blank
                else:
                    icon_line += separator + (en_space * 4)
            icon_line += self.get_emoji(destination,
                                        buff,
                                        fallback=True,
                                        fallback_fmt="{:1.1}",
                                        force_emoji=True)
            first = False
        groups = []
        for player in players:
            if player["group"] not in groups:
                groups.append(player["group"])
        if len(groups) > 1:
            players.sort(key=lambda p: p["group"])
        current_group = None
        for player in players:
            if "buffUptimes" not in player:
                continue
            if len(groups) > 1:
                if not current_group or player["group"] != current_group:
                    current_group = player["group"]
                    buff_lines.append(f"> **GROUP {current_group}**")
            line = "`"
            line = self.get_emoji(destination,
                                  player["profession"],
                                  fallback=True,
                                  fallback_fmt="",
                                  force_emoji=True)
            align = (3 + charater_name_max_length - len(player["name"])) * " "
            line += "`" + player["name"] + align
            for buff in buffs:
                for buff_uptime in player["buffUptimes"]:
                    if buff["id"] == buff_uptime["id"]:
                        uptime = str(
                            round(buff_uptime["buffData"][0]["uptime"],
                                  1)).rjust(5)
                        break
                else:
                    uptime = "0"
                if not buff["stacking"]:
                    uptime += "%"
                line += uptime
                line += separator + ((6 - len(uptime)) * magic_space)
            line += '`'
            buff_lines.append(line.strip())
        if not wvw:
            embed = embed_list_lines(embed, lines, "> **PLAYERS**")
        if wvw:
            dpses = dpses[:15]
        embed = embed_list_lines(embed, dpses, "> **DPS**")
        embed.add_field(name="> **BUFFS**", value=icon_line)

        embed = embed_list_lines(embed, buff_lines, zero_width_space)
        boss = self.gamedata["bosses"].get(str(data["triggerID"]))
        date_format = "%Y-%m-%d %H:%M:%S %z"
        date = datetime.datetime.strptime(data["timeEnd"] + "00", date_format)
        start_date = datetime.datetime.strptime(data["timeStart"] + "00",
                                                date_format)
        date = date.astimezone(datetime.timezone.utc)
        start_date = start_date.astimezone(datetime.timezone.utc)
        doc = {
            "boss_id": data["triggerID"],
            "start_date": start_date,
            "date": date,
            "players":
            sorted([player["account"] for player in data["players"]]),
            "permalink": permalink,
            "success": data["success"],
            "duration": duration_time
        }
        duplicate = await self.find_duplicate_dps_report(doc)
        if not duplicate:
            await self.db.encounters.insert_one(doc)
        embed.timestamp = date
        embed.set_footer(text="Recorded at",
                         icon_url=self.bot.user.display_avatar.url)
        if boss:
            embed.set_author(name=data["fightName"], icon_url=boss["icon"])
        return embed

    @app_commands.command(name="evtc")
    @app_commands.checks.bot_has_permissions(embed_links=True)
    @app_commands.describe(
        file="EVTC file to process. Accepted formats: .evtc, .zip, .zevtc")
    async def evtc(self, interaction: discord.Interaction,
                   file: discord.Attachment):
        """Process an EVTC combat log in an attachment"""
        if not file.filename.endswith(ALLOWED_FORMATS):
            return await interaction.response.send_message(
                "The attachment seems not to be of a correct filetype.\n"
                f"Allowed file extensions: `{', '.join(ALLOWED_FORMATS)}`",
                ephemeral=True)
        await interaction.response.defer()
        await self.process_evtc([file], interaction.user, interaction.followup)

    @evtc_automation_group.command(name="channel")
    @app_commands.guild_only()
    @app_commands.checks.has_permissions(manage_guild=True,
                                         manage_channels=True)
    @app_commands.checks.bot_has_permissions(embed_links=True,
                                             use_external_emojis=True)
    @app_commands.describe(
        enabled="Disable or enable this feature on the specificed channel",
        channel="The target channel",
        autodelete="Delete original message after processing the EVTC log")
    async def evtc_channel(self, interaction: discord.Interaction,
                           enabled: bool, channel: discord.TextChannel,
                           autodelete: bool):
        """Sets a channel to be automatically used to process EVTC logs
        posted within"""
        doc = await self.bot.database.get(channel, self)
        enabled = not doc.get("evtc.enabled", False)
        await self.bot.database.set(channel, {
            "evtc.enabled": enabled,
            "evtc.autodelete": autodelete
        }, self)
        if enabled:
            msg = ("Automatic EVTC processing enabled. Simply upload the file "
                   f"wish to be processed in {channel.mention}, while "
                   "@mentioning the bot in the same message.. Accepted "
                   f"formats: `{', '.join(ALLOWED_FORMATS)}`\nTo disable, use "
                   "this command again.")
            if not channel.permissions_for(interaction.guild.me).embed_links:
                msg += ("I won't be able to process logs without Embed "
                        "Links permission.")
        else:
            msg = ("Automatic EVTC processing diasbled")
        await interaction.response.send_message(msg)

    def generate_evtc_api_key(self) -> None:
        return secrets.token_urlsafe(64)

    async def get_user_evtc_api_key(self,
                                    user: discord.User) -> Union[str, None]:
        doc = await self.db.evtc.api_keys.find_one({"user": user.id}) or {}
        return doc.get("token", None)

    @evtc_automation_group.command(name="api_key")
    @app_commands.describe(operation="The operation to perform")
    @app_commands.choices(operation=[
        Choice(name="View your API key", value="view"),
        Choice(name="Generate or regenerate your API key", value="generate"),
        Choice(name="Delete your API key", value="delete")
    ])
    async def evtc_api_key(self, interaction: discord.Interaction,
                           operation: str):
        """Generate an API key for third-party apps that automatically upload EVTC logs"""
        await interaction.response.defer(ephemeral=True)
        existing_key = await self.get_user_evtc_api_key(interaction.user)
        if operation == "delete":
            if not existing_key:
                return await interaction.followup.send(
                    "You don't have an EVTC API key generated.")
            await self.db.evtc.api_keys.delete_one(
                {"_id": interaction.user.id})
            return await interaction.followup.send(
                "Your EVTC API key has been deleted.")
        if operation == "view":
            if not existing_key:
                return await interaction.followup.send(
                    "You don't have an EVTC API key generated. Use "
                    "`/evtc api_key generate` to generate one.")
            return await interaction.followup.send(
                f"Your EVTC API key is ```{existing_key}```")
        if operation == "generate":
            key = self.generate_evtc_api_key()
            await self.db.evtc.api_keys.insert_one({
                "user": interaction.user.id,
                "token": key
            })
            new = "new " if existing_key else ""
            return await interaction.followup.send(
                f"Your {new}new EVTC API key is:\n```{key}```You may use "
                "it with utilities that automatically upload logs to link "
                "them with your account, and potentially post them to "
                "certain channels. See `/evtc_automation` for more\n\nYou may "
                "revoke the key at any time with `/evtc api_key delete`, or "
                "regenerate it with `/evtc api_key generate`. Don't share "
                "this key with anyone.\nYou can also use this "
                "key without setting any upload destinations. Doing so will "
                "still append the report links to `/bosses` results")

    async def get_evtc_notification_channel(self, id, user):
        await self.db.evtc.channels.find_one({
            "channel_id": id,
            "user": user.id
        })

    @autopost_group.command(name="add_destination")
    @app_commands.checks.has_permissions(embed_links=True,
                                         use_external_emojis=True)
    @app_commands.checks.bot_has_permissions(embed_links=True,
                                             use_external_emojis=True)
    async def evtc_autoupload_add(self, interaction: discord.Interaction):
        """Add this channel as a personal destination to autopost EVTC logs to
        """
        await interaction.response.defer(ephemeral=True)
        channel = interaction.channel
        key = await self.get_user_evtc_api_key(interaction.user)
        if not key:
            return await interaction.followup.send(
                "You don't have an EVTC API key generated. Use "
                "`/evtc api_key generate` to generate one. Confused about "
                "what this is? The aforementioned command includes a tutorial")
        doc = await self.db.evtc.destinations.find_one(
            {
                "user_id": interaction.user.id,
                "channel_id": channel.id
            }) or {}
        if interaction.guild:
            channel_doc = await self.bot.database.get(channel, self)
            if channel_doc.get("evtc.disabled", False):
                return await interaction.followup.send(
                    "This channel is disabled for EVTC processing.")
        if doc:
            return await interaction.followup.send(
                "This channel is already a destination. If you're "
                "looking to remove it, see "
                "`/evtc_automation remove_destinations`")
        results = await self.call_api("account", interaction.user, ["account"])
        guild_ids = results.get("guilds")
        if guild_ids:
            endpoints = [f"guild/{gid}" for gid in guild_ids]
            guilds = await self.call_multiple(endpoints)
        view = EvtcGuildSelectionView(self, guilds)
        await interaction.followup.send(
            "If you wish to use this channel to post only "
            "the logs made while representing a specific guild, select "
            "them from the list below. Otherwise, click `Next`.",
            view=view,
            ephemeral=True)
        if await view.wait():
            return
        await self.db.evtc.destinations.insert_one({
            "user_id":
            interaction.user.id,
            "channel_id":
            channel.id,
            "guild_ids":
            view.selected_guilds,
            "guild_tags": [
                guild["tag"] for guild in guilds
                if guild["id"] in view.selected_guilds
            ]
        })

    @autopost_group.command(name="remove_destinations")
    async def evtc_autoupload_remove(self, interaction: discord.Interaction):
        """Remove chosen EVTC autoupload destinations"""
        await interaction.response.defer(ephemeral=True)
        destinations = await self.db.evtc.destinations.find({
            "user_id":
            interaction.user.id
        }).to_list(None)
        channels = [
            self.bot.get_channel(dest["channel_id"]) for dest in destinations
        ]
        if not channels:
            return await interaction.followup.send(
                "You don't have any autopost destinations yet.")
        view = discord.ui.View()
        view.add_item(
            EvtcAutouploadDestinationsSelect(self, channels, destinations))
        await interaction.followup.send("** **", view=view)

    async def process_evtc(self, files: list[discord.Attachment], user,
                           destination):
        embeds = []
        for attachment in files:
            if attachment.filename.endswith(ALLOWED_FORMATS):
                try:
                    resp = await self.upload_log(attachment, user)
                    data = await self.get_encounter_data(resp["id"])
                    embeds.append(await
                                  self.upload_embed(destination, data,
                                                    resp["permalink"]))
                except Exception as e:
                    self.log.exception("Exception processing EVTC log ",
                                       exc_info=e)
                    return await destination.send(
                        content="Error processing your log! :x:",
                        ephemeral=True)
        for embed in embeds:
            await destination.send(embed=embed)

    @tasks.loop(seconds=5)
    async def post_evtc_notifications(self):
        cursor = self.db.evtc.notifications.find({"posted": False})
        async for doc in cursor:
            try:
                user = self.bot.get_user(doc["user_id"])
                destinations = await self.db.evtc.destinations.find({
                    "user_id":
                    user.id
                }).to_list(None)
                for destination in destinations:
                    destination["channel"] = self.bot.get_channel(
                        destination["channel_id"])
                data = await self.get_encounter_data(doc["encounter_id"])
                recorded_by = data.get("recordedBy", None)
                recorded_player_guild = None
                for player in data["players"]:
                    if player["name"] == recorded_by:
                        recorded_player_guild = player.get("guildID")
                        break
                embed = await self.upload_embed(None, data, doc["permalink"])
                embed.set_footer(
                    text="Autoposted by "
                    f"{user.name}#{user.discriminator}({user.id})."
                    " The bot respects the user's permissions; remove their "
                    "permission to send messages or embed "
                    "links to stop these messsages.")
                for destination in destinations:
                    try:
                        channel = destination["channel"]
                        if destination[
                                "guild_ids"] and recorded_by and recorded_player_guild:
                            if destination["guild_ids"]:
                                if recorded_player_guild not in destination[
                                        "guild_ids"]:
                                    continue
                        if not channel:
                            continue
                        has_permission = False
                        if guild := channel.guild:
                            members = [
                                channel.guild.me,
                                guild.get_member(user.id)
                            ]
                            for member in members:
                                if not channel.permissions_for(
                                        member).embed_links:
                                    break
                                if not channel.permissions_for(
                                        member).send_messages:
                                    break
                                if not channel.permissions_for(
                                        member).use_external_emojis:
                                    break
                            else:
                                has_permission = True
                        else:
                            has_permission = True
                    except asyncio.TimeoutError:
                        raise
                    except Exception as e:
                        self.log.exception(
                            "Exception during evtc notificaitons", exc_info=e)
                        continue
                    if has_permission:
                        try:
                            await channel.send(embed=embed)
                        except discord.HTTPException as e:
                            self.log.exception(e)
                            continue
            except asyncio.CancelledError:
                raise
Exemple #15
0
class GeneralGuild:

    guild_group = app_commands.Group(name="guild",
                                     description="Guild related commands")

    async def guild_name_autocomplete(self, interaction: discord.Interaction,
                                      current: str):
        doc = await self.bot.database.get(interaction.user, self)
        key = doc.get("key", {})
        if not key:
            return []
        account_key = key["account_name"].replace(".", "_")

        async def cache_guild():
            try:
                results = await self.call_api("account",
                                              scopes=["account"],
                                              key=key["key"])
            except APIError:
                return choices
            guild_ids = results.get("guilds", [])
            endpoints = [f"guild/{gid}" for gid in guild_ids]
            try:
                guilds = await self.call_multiple(endpoints)
            except APIError:
                return choices
            guild_list = []
            for guild in guilds:
                guild_list.append({"name": guild["name"], "id": guild["id"]})
            c = {
                "last_update": datetime.datetime.utcnow(),
                "guild_list": guild_list
            }
            await self.bot.database.set(interaction.user,
                                        {f"guild_cache.{account_key}": c},
                                        self)

        choices = []
        current = current.lower()
        if interaction.guild:
            doc = await self.bot.database.get(interaction.guild, self)
            guild_id = doc.get("guild_ingame")
            if guild_id:
                choices.append(
                    Choice(name="Server's default guild", value=guild_id))
        doc = await self.bot.database.get(interaction.user, self)
        if not key:
            return choices
        cache = doc.get("guild_cache", {}).get(account_key, {})
        if not cache:
            if not choices:
                cache = await cache_guild()
            else:
                asyncio.create_task(cache_guild())
        elif cache["last_update"] < datetime.datetime.utcnow(
        ) - datetime.timedelta(days=7):
            asyncio.create_task(cache_guild())
        if cache:
            choices += [
                Choice(name=guild["name"], value=guild["id"])
                for guild in cache["guild_list"]
                if current in guild["name"].lower()
            ]
        return choices

    @guild_group.command(name="info")
    @app_commands.describe(guild="Guild name.")
    @app_commands.autocomplete(guild=guild_name_autocomplete)
    async def guild_info(self, interaction: discord.Interaction, guild: str):
        """General guild stats"""
        endpoint = "guild/" + guild
        await interaction.response.defer()
        try:
            results = await self.call_api(endpoint, interaction.user,
                                          ["guilds"])
        except (IndexError, APINotFound):
            return await interaction.followup.send_message(
                "Invalid guild name", ephemeral=True)
        except APIForbidden:
            return await interaction.followup.send_message(
                "You don't have enough permissions in game to "
                "use this command",
                ephemeral=True)
        data = discord.Embed(description='General Info about {0}'.format(
            results["name"]),
                             colour=await self.get_embed_color(interaction))
        data.set_author(name="{} [{}]".format(results["name"], results["tag"]))
        guild_currencies = [
            "influence", "aetherium", "resonance", "favor", "member_count"
        ]
        for cur in guild_currencies:
            if cur == "member_count":
                data.add_field(name='Members',
                               value="{} {}/{}".format(
                                   self.get_emoji(interaction, "friends"),
                                   results["member_count"],
                                   str(results["member_capacity"])))
            else:
                data.add_field(name=cur.capitalize(),
                               value='{} {}'.format(
                                   self.get_emoji(interaction, cur),
                                   results[cur]))
        if "motd" in results:
            data.add_field(name='Message of the day:',
                           value=results["motd"],
                           inline=False)
        data.set_footer(text='A level {} guild'.format(results["level"]))
        await interaction.followup.send(embed=data)

    @guild_group.command(name="members")
    @app_commands.describe(guild="Guild name.")
    @app_commands.autocomplete(guild=guild_name_autocomplete)
    async def guild_members(self, interaction: discord.Interaction,
                            guild: str):
        """Shows a list of members and their ranks."""
        user = interaction.user
        scopes = ["guilds"]
        await interaction.response.defer()
        endpoints = [
            f"guild/{guild}", f"guild/{guild}/members", f"guild/{guild}/ranks"
        ]
        try:
            base, results, ranks = await self.call_multiple(
                endpoints, user, scopes)
        except (IndexError, APINotFound):
            return await interaction.followup.send("Invalid guild name",
                                                   ephemeral=True)
        except APIForbidden:
            return await interaction.followup.send(
                "You don't have enough permissions in game to "
                "use this command",
                ephemeral=True)
        embed = discord.Embed(
            colour=await self.get_embed_color(interaction),
            title=base["name"],
        )
        order_id = 1
        # For each order the rank has, go through each member and add it with
        # the current order increment to the embed
        lines = []

        async def get_guild_member_mention(account_name):
            if not interaction.guild:
                return ""
            cursor = self.bot.database.iter(
                "users", {
                    "$or": [{
                        "cogs.GuildWars2.key.account_name": account_name
                    }, {
                        "cogs.GuildWars2.keys.account_name": account_name
                    }]
                })
            async for doc in cursor:
                member = interaction.guild.get_member(doc["_id"])
                if member:
                    return member.mention
            return ""

        embeds = []
        for order in ranks:
            for member in results:
                # Filter invited members
                if member['rank'] != "invited":
                    member_rank = member['rank']
                    # associate order from /ranks with rank from /members
                    for rank in ranks:
                        if member_rank == rank['id']:
                            if rank['order'] == order_id:
                                mention = await get_guild_member_mention(
                                    member["name"])
                                if mention:
                                    mention = f" - {mention}"
                                line = "**{}**{}\n*{}*".format(
                                    member['name'], mention, member['rank'])
                                if len(str(lines)) + len(line) < 6000:
                                    lines.append(line)
                                else:
                                    embeds.append(
                                        embed_list_lines(embed,
                                                         lines,
                                                         "> **MEMBERS**",
                                                         inline=True))
                                    lines = [line]
                                    embed = discord.Embed(
                                        title=base["name"],
                                        colour=await
                                        self.get_embed_color(interaction))

            order_id += 1
        embeds.append(
            embed_list_lines(embed, lines, "> **MEMBERS**", inline=True))
        if len(embeds) == 1:
            return await interaction.followup.send(embed=embed)
        for i, embed in enumerate(embeds, start=1):
            embed.set_footer(text="Page {}/{}".format(i, len(embeds)))
        view = PaginatedEmbeds(embeds, interaction.user)
        out = await interaction.followup.send(embed=embeds[0], view=view)
        view.response = out

    @guild_group.command(name="treasury")
    @app_commands.describe(guild="Guild name.")
    @app_commands.autocomplete(guild=guild_name_autocomplete)
    async def guild_treasury(self, interaction: discord.Interaction,
                             guild: str):
        """Get list of current and needed items for upgrades"""
        await interaction.response.defer()
        endpoints = [f"guild/{guild}", f"guild/{guild}/treasury"]
        try:
            base, treasury = await self.call_multiple(endpoints,
                                                      interaction.user,
                                                      ["guilds"])
        except (IndexError, APINotFound):
            return await interaction.followup.send("Invalid guild name",
                                                   ephemeral=True)
        except APIForbidden:
            return await interaction.followup.send(
                "You don't have enough permissions in game to "
                "use this command",
                ephemeral=True)
        embed = discord.Embed(description=zero_width_space,
                              colour=await self.get_embed_color(interaction))
        embed.set_author(name=base["name"])
        item_counter = 0
        amount = 0
        lines = []
        itemlist = []
        for item in treasury:
            res = await self.fetch_item(item["item_id"])
            itemlist.append(res)
        # Collect amounts
        if treasury:
            for item in treasury:
                current = item["count"]
                item_name = itemlist[item_counter]["name"]
                needed = item["needed_by"]
                for need in needed:
                    amount = amount + need["count"]
                if amount != current:
                    line = "**{}**\n*{}*".format(
                        item_name,
                        str(current) + "/" + str(amount))
                    if len(str(lines)) + len(line) < 6000:
                        lines.append(line)
                amount = 0
                item_counter += 1
        else:
            return await interaction.followup.send("Treasury is empty!")
        embed = embed_list_lines(embed, lines, "> **TREASURY**", inline=True)
        await interaction.followup.send(embed=embed)

    @guild_group.command(name="log")
    @app_commands.describe(log_type="Select the type of log to inspect",
                           guild="Guild name.")
    @app_commands.choices(log_type=[
        Choice(name=it.title(), value=it)
        for it in ["stash", "treasury", "members"]
    ])
    @app_commands.autocomplete(guild=guild_name_autocomplete)
    async def guild_log(self, interaction: discord.Interaction, log_type: str,
                        guild: str):
        """Get log of last 20 entries of stash/treasury/members"""
        member_list = [
            "invited", "joined", "invite_declined", "rank_change", "kick"
        ]
        # TODO use account cache to speed this up
        await interaction.response.defer()
        endpoints = [f"guild/{guild}", f"guild/{guild}/log/"]
        try:
            base, log = await self.call_multiple(endpoints, interaction.user,
                                                 ["guilds"])
        except (IndexError, APINotFound):
            return await interaction.followup.send("Invalid guild name",
                                                   ephemeral=True)
        except APIForbidden:
            return await interaction.followup.send(
                "You don't have enough permissions in game to "
                "use this command",
                ephemeral=True)

        data = discord.Embed(description=zero_width_space,
                             colour=await self.get_embed_color(interaction))
        data.set_author(name=base["name"])
        lines = []
        length_lines = 0
        for entry in log:
            if entry["type"] == log_type:
                time = entry["time"]
                timedate = datetime.datetime.strptime(
                    time, "%Y-%m-%dT%H:%M:%S.%fZ").strftime('%d.%m.%Y %H:%M')
                user = entry["user"]
                if log_type == "stash" or log_type == "treasury":
                    quantity = entry["count"]
                    if entry["item_id"] == 0:
                        item_name = self.gold_to_coins(interaction,
                                                       entry["coins"])
                        quantity = ""
                        multiplier = ""
                    else:
                        itemdoc = await self.fetch_item(entry["item_id"])
                        item_name = itemdoc["name"]
                        multiplier = "x"
                    if log_type == "stash":
                        if entry["operation"] == "withdraw":
                            operator = " withdrew"
                        else:
                            operator = " deposited"
                    else:
                        operator = " donated"
                    line = "**{}**\n*{}*".format(
                        timedate, user + "{} {}{} {}".format(
                            operator, quantity, multiplier, item_name))
                    if length_lines + len(line) < 5500:
                        length_lines += len(line)
                        lines.append(line)
            if log_type == "members":
                entry_string = ""
                if entry["type"] in member_list:
                    time = entry["time"]
                    timedate = datetime.datetime.strptime(
                        time,
                        "%Y-%m-%dT%H:%M:%S.%fZ").strftime('%d.%m.%Y %H:%M')
                    user = entry["user"]
                    if entry["type"] == "invited":
                        invited_by = entry["invited_by"]
                        entry_string = (f"{invited_by} has "
                                        "invited {user} to the guild.")
                    elif entry["type"] == "joined":
                        entry_string = f"{user} has joined the guild."
                    elif entry["type"] == "kick":
                        kicked_by = entry["kicked_by"]
                        if kicked_by == user:
                            entry_string = "{} has left the guild.".format(
                                user)
                        else:
                            entry_string = "{} has been kicked by {}.".format(
                                user, kicked_by)
                    elif entry["type"] == "rank_change":
                        old_rank = entry["old_rank"]
                        new_rank = entry["new_rank"]
                        if "changed_by" in entry:
                            changed_by = entry["changed_by"]
                            entry_string = (
                                f"{changed_by} has changed "
                                f"the role of {user} from {old_rank} "
                                f"to {new_rank}.")
                            entry_string = (
                                "{user} changed his "
                                "role from {old_rank} to {new_rank}.")
                    line = "**{}**\n*{}*".format(timedate, entry_string)
                    if length_lines + len(line) < 5500:
                        length_lines += len(line)
                        lines.append(line)
        if not lines:
            return await interaction.followup.send(
                "No {} log entries yet for {}".format(log_type, base["name"]))
        data = embed_list_lines(data, lines,
                                "> **{0} Log**".format(log_type.capitalize()))
        await interaction.followup.send(embed=data)

    @guild_group.command(name="default")
    @app_commands.describe(guild="Guild name")
    @app_commands.autocomplete(guild=guild_name_autocomplete)
    async def guild_default(self, interaction: discord.Interaction,
                            guild: str):
        """ Set your default guild for guild commands on this server."""
        await interaction.response.defer()
        results = await self.call_api(f"guild/{guild}")
        await self.bot.database.set_guild(interaction.guild, {
            "guild_ingame": guild,
        }, self)
        await interaction.followup.send(
            f"Your default guild is now set to {results['name']} for this "
            "server. All commands from the `guild` command group "
            "invoked without a specified guild will default to "
            "this guild. To reset, simply invoke this command "
            "without specifying a guild")
Exemple #16
0
class KeyMixin:

    key_group = app_commands.Group(name="key",
                                   description="Character related commands")

    @key_group.command(name="add")
    @app_commands.describe(
        token="Generate at https://account.arena.net under Applications tab")
    async def key_add(self, interaction: discord.Interaction, token: str):
        """Adds a key and associates it with your discord account"""
        await interaction.response.defer(ephemeral=True)
        doc = await self.bot.database.get(interaction.user, self)
        try:
            endpoints = ["tokeninfo", "account"]
            token_info, acc = await self.call_multiple(endpoints, key=token)
        except APIInactiveError:
            return await interaction.followup.send(
                "The API is currently down. "
                "Try again later. ")
        except APIError:
            return await interaction.followup.send("The key is invalid.")
        key_doc = {
            "key": token,
            "account_name": acc["name"],
            "name": token_info["name"],
            "permissions": token_info["permissions"]
        }
        # at this point we know the key is valid
        keys = doc.get("keys", [])
        key = doc.get("key", {})
        if not keys and key:
            # user already had a key but it isn't in keys
            # (existing user) so add it
            keys.append(key)
        if key_doc["key"] in [k["key"] for k in keys]:
            return await interaction.followup.send(
                "You have already added this key before.")
        if len(keys) >= 15:
            return await interaction.followup.send(
                "You've reached the maximum limit of "
                "15 API keys, please remove one before adding "
                "another")
        keys.append(key_doc)
        await self.bot.database.set(interaction.user, {
            "key": key_doc,
            "keys": keys
        }, self)
        if len(keys) > 1:
            output = ("Your key was verified and "
                      "added to your list of keys, you can swap between "
                      "them at any time using /key switch.")
        else:
            output = ("Your key was verified and "
                      "associated with your account.")
        all_permissions = ("account", "builds", "characters", "guilds",
                           "inventories", "progression", "pvp", "tradingpost",
                           "unlocks", "wallet")
        missing = [
            x for x in all_permissions if x not in key_doc["permissions"]
        ]
        if missing:
            output += ("\nPlease note that your API key doesn't have the "
                       "following permissions checked: "
                       f"```{', '.join(missing)}```\nSome commands "
                       "will not work. Consider adding a new key with "
                       "those permissions checked.")

        await interaction.followup.send(output)
        try:
            if interaction.guild:
                await self.worldsync_on_member_join(interaction.user)
                await self.guildsync_on_member_join(interaction.user)
                await self.key_sync_user(interaction.user)
                return
            for guild in self.bot.guilds:
                try:
                    if len(guild.members) > 5000:
                        continue
                    if interaction.user not in guild.members:
                        continue
                    member = guild.get_member(interaction.user.id)
                    await self.key_sync_user(member)
                    doc = await self.bot.database.get(guild, self)
                    worldsync = doc.get("worldsync", {})
                    worldsync_enabled = worldsync.get("enabled", False)
                    if worldsync_enabled:
                        await self.worldsync_on_member_join(member)
                    guildsync = doc.get("sync", {})
                    if guildsync.get("on", False) and guildsync.get(
                            "setupdone", False):
                        await self.guildsync_on_member_join(member)
                except Exception:
                    pass
        except Exception:
            pass

    async def key_autocomplete(self, interaction: discord.Interaction,
                               current: str):
        doc = await self.bot.database.get(interaction.user, self)
        current = current.lower()
        choices = []
        keys = doc.get("keys", [])
        key = doc.get("key", {})
        if not keys and key:
            keys.append(key)
        for key in keys:
            token_name = f"{key['account_name']} - {key['name']}"
            choices.append(Choice(name=token_name, value=key["key"]))
        return [choice for choice in choices if current in choice.name.lower()]

    @key_group.command(name="remove")
    @app_commands.describe(token="The API key to remove from your account")
    @app_commands.autocomplete(token=key_autocomplete)
    async def key_remove(self, interaction: discord.Interaction, token: str):
        """Remove selected keys from the bot"""
        await interaction.response.defer(ephemeral=True)
        doc = await self.bot.database.get(interaction.user, self)
        keys = doc.get("keys", [])
        key = doc.get("key", {})
        to_keep = []
        if key.get("key") == token:
            key = {}
        for k in keys:
            if k["key"] != token:
                to_keep.append(k)
        if key == key and to_keep == keys:
            return await interaction.followup.send(
                "No keys were removed. Invalid token")
        await self.bot.database.set(interaction.user, {
            "key": key,
            "keys": to_keep
        }, self)
        await interaction.followup.send("Key removed.")

    @key_group.command(name="info")
    async def key_info(self, interaction: discord.Interaction):
        """Information about your api keys"""
        doc = await self.bot.database.get(interaction.user, self)
        await interaction.response.defer(ephemeral=True)
        keys = doc.get("keys", [])
        key = doc.get("key", {})
        if not keys and not key:
            return await interaction.followup.send(
                "You have no keys added, you can add one with /key add.")
        embed = await self.display_keys(interaction,
                                        doc,
                                        display_active=True,
                                        show_tokens=True,
                                        reveal_tokens=True)
        await interaction.followup.send(embed=embed)

    @key_group.command(name="switch")
    @app_commands.autocomplete(token=key_autocomplete)
    async def key_switch(self, interaction: discord.Interaction, token: str):
        """Swaps between multiple stored API keys."""
        doc = await self.bot.database.get(interaction.user, self)
        keys = doc.get("keys", [])
        key = doc.get("key", {})
        if not keys:
            return await interaction.response.send_message(
                "You need to add additional API keys first using /key "
                "add first.",
                ephemeral=True)
        if key["key"] == token:
            return await interaction.response.send_message(
                "That key is currently active.", ephemeral=True)
        for k in keys:
            if k["key"] == token:
                break
        else:
            return await interaction.response.send_message(
                "That key is not in your account.", ephemeral=True)
        await self.bot.database.set(interaction.user, {"key": k}, self)
        msg = "Swapped to selected key."
        if key["name"]:
            msg += " Name : `{}`".format(k["name"])
        await interaction.response.send_message(msg, ephemeral=True)

    async def display_keys(self,
                           interaction: discord.Interaction,
                           doc,
                           *,
                           display_active=False,
                           display_permissions=True,
                           show_tokens=False,
                           reveal_tokens=False):
        def get_value(key):
            lines = []
            if display_permissions:
                lines.append("Permissions: " + ", ".join(key["permissions"]))
            if show_tokens:
                token = key["key"]
                if not reveal_tokens:
                    token = token[:7] + re.sub("[a-zA-Z0-9]", r"\*", token[8:])
                else:
                    token = f"||{token}||"
                lines.append(token)
            return "\n".join(lines)

        keys = doc.get("keys", [])
        embed = discord.Embed(title="Your keys",
                              color=await self.get_embed_color(interaction))
        embed.set_author(name=interaction.user.name,
                         icon_url=interaction.user.display_avatar.url)
        if display_active:
            active_key = doc.get("key", {})
            if active_key:
                name = "**Active key**: {}".format(active_key["account_name"])
                token_name = active_key["name"]
                if token_name:
                    name += " - " + token_name
                embed.add_field(name=name, value=get_value(active_key))
        for counter, key in enumerate(keys, start=1):
            name = "**{}**: {}".format(counter, key["account_name"])
            token_name = key["name"]
            if token_name:
                name += " - " + token_name
            embed.add_field(name=name, value=get_value(key), inline=False)
        embed.set_footer(text=self.bot.user.name,
                         icon_url=self.bot.user.display_avatar.url)
        return embed
Exemple #17
0
class NotiifiersMixin:
    notifier_group = app_commands.Group(name="notifier",
                                        description="Notifier Commands")

    @notifier_group.command(name="daily")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.guild_only()
    @app_commands.describe(
        enabled="Enable or disable Daily Notifier. If "
        "enabling, channel argument must be set",
        channel="The channel to post to.",
        pin_message="Toggle whether to "
        "automatically pin the daily message or not.",
        behavior="Select additional behavior for "
        "deleting/editing the message. Leave blank for standard behavior.")
    @app_commands.choices(behavior=[
        Choice(
            name="Delete the previous day's message and post a new message. "
            "Causes an unread notification",
            value="autodelete"),
        Choice(name="Edit the previous day's message. No unread notification.",
               value="autoedit")
    ])
    async def daily_notifier(self,
                             interaction: discord.Interaction,
                             enabled: bool,
                             channel: discord.TextChannel = None,
                             pin_message: bool = False,
                             behavior: str = None):
        """Send daily achievements to a channel every day"""
        doc = await self.bot.database.get(interaction.guild, self)
        enabled = doc.get("daily", {}).get("on", False)

        # IF ENABLED AND NOT CHANNEL

        if not enabled and not channel:
            return await interaction.response.send_message(
                "Daily notifier is aleady disabled. If "
                "you were trying to enable it, make sure to fill out "
                "the `channel` argument.",
                ephemeral=True)
        if enabled and not channel:
            await self.bot.database.set(interaction.guild, {"daily.on": False},
                                        self)
            return await interaction.response.send_message(
                "Daily notifier disabled.")
        if not channel.permissions_for(interaction.guild.me).send_messages:
            return await interaction.response.send_message(
                "I do not have permissions to send "
                f"messages to {channel.mention}",
                ephemeral=True)
        if not channel.permissions_for(interaction.guild.me).embed_links:
            return await interaction.response.send_message(
                "I do not have permissions to embed links in "
                f"{channel.mention}",
                ephemeral=True)
        view = discord.ui.View(timeout=60)
        view.add_item(
            DailyCategoriesDropdown(interaction, self, behavior, pin_message,
                                    channel))
        await interaction.response.send_message("** **",
                                                view=view,
                                                ephemeral=True)
        if await view.wait():
            return await interaction.followup.edit_message(
                content="No response in time.", view=None)

    @notifier_group.command(name="news")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.describe(
        enabled="Enable or disable game news notifier. If "
        "enabling, channel argument must be set",
        channel="The channel to post to.",
        mention="The role to ping when posting the notification.")
    async def newsfeed(self,
                       interaction: discord.Interaction,
                       enabled: bool,
                       channel: discord.TextChannel = None,
                       mention: discord.Role = None):
        """Automatically sends news from guildwars2.com to a specified channel
        """
        if enabled and not channel:
            return await interaction.response.send_message(
                "You must specify a channel.", ephemeral=True)
        doc = await self.bot.database.get(interaction.guild, self)
        already_enabled = doc.get("news", {}).get("on", False)
        if not already_enabled and not channel:
            return await interaction.response.send_message(
                "News notifier is aleady disabled. If "
                "you were trying to enable it, make sure to fill out "
                "the `channel` argument.",
                ephemeral=True)
        if already_enabled and not channel:
            await self.bot.database.set(interaction.guild, {"news.on": False},
                                        self)
            return await interaction.response.send_message(
                "News notifier disabled.", ephemeral=True)
        if not channel.permissions_for(interaction.guild.me).send_messages:
            return await interaction.response.send_message(
                "I do not have permissions to send "
                f"messages to {channel.mention}",
                ephemeral=True)
        if not channel.permissions_for(interaction.guild.me).embed_links:
            return await interaction.response.send_message(
                "I do not have permissions to embed links in "
                f"{channel.mention}",
                ephemeral=True)
        role_id = mention.id if mention else None
        settings = {
            "news.on": True,
            "news.channel": channel.id,
            "news.role": role_id
        }
        await self.bot.database.set(interaction.guild, settings, self)
        await interaction.response.send_message(
            f"I will now send news to {channel.mention}.")

    @notifier_group.command(name="update")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.guild_only()
    @app_commands.describe(
        enabled="Enable or disable game update notifier. If "
        "enabling, channel argument must be set",
        channel="The channel to post to.",
        mention="The role to ping when posting the notification.")
    async def updatenotifier(self,
                             interaction: discord.Interaction,
                             enabled: bool,
                             channel: discord.TextChannel = None,
                             mention: discord.Role = None):
        """Send a notification whenever the game is updated"""
        if enabled and not channel:
            return await interaction.response.send_message(
                "You must specify a channel.", ephemeral=True)
        if not interaction.guild:
            return await interaction.response.send_message(
                "This command can only be used in servers at the time.",
                ephemeral=True)
        doc = await self.bot.database.get(interaction.guild, self)
        enabled = doc.get("updates", {}).get("on", False)
        if not enabled and not channel:
            return await interaction.response.send_message(
                "Update notifier is aleady disabled. If "
                "you were trying to enable it, make sure to fill out "
                "the `channel` argument.",
                ephemeral=True)
        if enabled and not channel:
            await self.bot.database.set(interaction.guild,
                                        {"updates.on": False}, self)
            return await interaction.response.send_message(
                "Update notifier disabled.")
        mention_string = ""
        if mention:
            mention = int(mention)
            if mention == interaction.guild.id:
                mention = "@everyone"
            elif role := interaction.guild.get_role(mention):
                mention_string = role.mention
            else:
                mention_string = interaction.guild.get_member(mention).mention

        settings = {
            "updates.on": True,
            "updates.channel": channel.id,
            "updates.mention": mention_string
        }
        await self.bot.database.set(interaction.guild, settings, self)
        await interaction.response.send_message(
            f"I will now send update notifications to {channel.mention}.")
Exemple #18
0
class PvpMixin:
    pvp_group = app_commands.Group(name="pvp",
                                   description="PvP related commands")

    @pvp_group.command(name="stats")
    async def pvp_stats(self, interaction: discord.Interaction):
        """Information about your general pvp stats"""
        await interaction.response.defer()
        doc = await self.fetch_key(interaction.user, ["pvp"])
        results = await self.call_api("pvp/stats", key=doc["key"])
        rank = results["pvp_rank"] + results["pvp_rank_rollovers"]
        totalgamesplayed = sum(results["aggregate"].values())
        totalwins = results["aggregate"]["wins"] + results["aggregate"]["byes"]
        if totalgamesplayed != 0:
            totalwinratio = int((totalwins / totalgamesplayed) * 100)
        else:
            totalwinratio = 0
        rankedgamesplayed = sum(results["ladders"]["ranked"].values())
        rankedwins = results["ladders"]["ranked"]["wins"] + \
            results["ladders"]["ranked"]["byes"]
        if rankedgamesplayed != 0:
            rankedwinratio = int((rankedwins / rankedgamesplayed) * 100)
        else:
            rankedwinratio = 0
        rank_id = results["pvp_rank"] // 10 + 1
        ranks = await self.call_api("pvp/ranks/{0}".format(rank_id))
        embed = discord.Embed(colour=await self.get_embed_color(interaction))
        embed.add_field(name="Rank", value=rank, inline=False)
        embed.add_field(name="Total games played", value=totalgamesplayed)
        embed.add_field(name="Total wins", value=totalwins)
        embed.add_field(name="Total winratio",
                        value="{}%".format(totalwinratio))
        embed.add_field(name="Ranked games played", value=rankedgamesplayed)
        embed.add_field(name="Ranked wins", value=rankedwins)
        embed.add_field(name="Ranked winratio",
                        value="{}%".format(rankedwinratio))
        embed.set_author(name=doc["account_name"])
        embed.set_thumbnail(url=ranks["icon"])
        await interaction.followup.send(embed=embed)

    @pvp_group.command(name="professions")
    @app_commands.describe(
        profession="Select a profession to view specific statistics")
    @app_commands.choices(profession=[
        Choice(name=p.title(), value=p) for p in [
            "warrior",
            "guardian",
            "revenant",
            "thief",
            "ranger",
            "engineer",
            "elementalist",
            "necromancer",
            "mesmer",
        ]
    ])
    async def pvp_professions(self,
                              interaction: discord.Interaction,
                              profession: str = None):
        """Information about your pvp profession stats."""
        await interaction.response.defer()
        doc = await self.fetch_key(interaction.user, ["pvp"])
        results = await self.call_api("pvp/stats", key=doc["key"])
        professions = self.gamedata["professions"].keys()
        professionsformat = {}
        if not profession:
            for profession in professions:
                if profession in results["professions"]:
                    wins = (results["professions"][profession]["wins"] +
                            results["professions"][profession]["byes"])
                    total = sum(results["professions"][profession].values())
                    winratio = int((wins / total) * 100)
                    professionsformat[profession] = {
                        "wins": wins,
                        "total": total,
                        "winratio": winratio
                    }
            mostplayed = max(professionsformat,
                             key=lambda i: professionsformat[i]['total'])
            icon = self.gamedata["professions"][mostplayed]["icon"]
            mostplayedgames = professionsformat[mostplayed]["total"]
            highestwinrate = max(
                professionsformat,
                key=lambda i: professionsformat[i]["winratio"])
            highestwinrategames = professionsformat[highestwinrate]["winratio"]
            leastplayed = min(professionsformat,
                              key=lambda i: professionsformat[i]["total"])
            leastplayedgames = professionsformat[leastplayed]["total"]
            lowestestwinrate = min(
                professionsformat,
                key=lambda i: professionsformat[i]["winratio"])
            lowestwinrategames = professionsformat[lowestestwinrate][
                "winratio"]
            data = discord.Embed(description="Professions",
                                 color=await self.get_embed_color(interaction))
            data.set_thumbnail(url=icon)
            data.add_field(name="Most played profession",
                           value="{0}, with {1}".format(
                               mostplayed.capitalize(), mostplayedgames))
            data.add_field(name="Highest winrate profession",
                           value="{0}, with {1}%".format(
                               highestwinrate.capitalize(),
                               highestwinrategames))
            data.add_field(name="Least played profession",
                           value="{0}, with {1}".format(
                               leastplayed.capitalize(), leastplayedgames))
            data.add_field(name="Lowest winrate profession",
                           value="{0}, with {1}%".format(
                               lowestestwinrate.capitalize(),
                               lowestwinrategames))
            data.set_author(name=doc["account_name"])
            return await interaction.followup.send(embed=data)
        wins = (results["professions"][profession]["wins"] +
                results["professions"][profession]["byes"])
        total = sum(results["professions"][profession].values())
        winratio = int((wins / total) * 100)
        color = self.gamedata["professions"][profession]["color"]
        color = int(color, 0)
        data = discord.Embed(description=f"Stats for {profession}",
                             colour=color)
        data.set_thumbnail(
            url=self.gamedata["professions"][profession]["icon"])
        data.add_field(name="Total games played", value="{0}".format(total))
        data.add_field(name="Wins", value="{0}".format(wins))
        data.add_field(name="Winratio", value="{0}%".format(winratio))
        data.set_author(name=doc["account_name"])
        await interaction.followup.send(embed=data)
Exemple #19
0
    def __new__(cls, *args: Any, **kwargs: Any) -> Self:
        # For issue 426, we need to store a copy of the command objects
        # since we modify them to inject `self` to them.
        # To do this, we need to interfere with the Cog creation process.
        self = super().__new__(cls)
        cmd_attrs = cls.__cog_settings__

        # Either update the command with the cog provided defaults or copy it.
        # r.e type ignore, type-checker complains about overriding a ClassVar
        self.__cog_commands__ = tuple(
            c._update_copy(cmd_attrs)
            for c in cls.__cog_commands__)  # type: ignore

        lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__}

        # Register the application commands
        children: List[Union[app_commands.Group,
                             app_commands.Command[Self, ..., Any]]] = []

        if cls.__cog_is_app_commands_group__:
            group = app_commands.Group(
                name=cls.__cog_group_name__,
                description=cls.__cog_group_description__,
                nsfw=cls.__cog_group_nsfw__,
                parent=None,
                guild_ids=getattr(cls,
                                  '__discord_app_commands_default_guilds__',
                                  None),
                guild_only=getattr(cls, '__discord_app_commands_guild_only__',
                                   False),
                default_permissions=getattr(
                    cls, '__discord_app_commands_default_permissions__', None),
            )
        else:
            group = None

        self.__cog_app_commands_group__ = group

        # Update the Command instances dynamically as well
        for command in self.__cog_commands__:
            setattr(self, command.callback.__name__, command)
            parent = command.parent
            if parent is not None:
                # Get the latest parent reference
                parent = lookup[parent.qualified_name]  # type: ignore

                # Update our parent's reference to our self
                parent.remove_command(command.name)  # type: ignore
                parent.add_command(command)  # type: ignore

            if hasattr(command, '__commands_is_hybrid__') and parent is None:
                app_command: Optional[
                    Union[app_commands.Group,
                          app_commands.Command[Self, ..., Any]]] = getattr(
                              command, 'app_command', None)
                if app_command:
                    group_parent = self.__cog_app_commands_group__
                    app_command = app_command._copy_with(parent=group_parent,
                                                         binding=self)
                    # The type checker does not see the app_command attribute even though it exists
                    command.app_command = app_command  # type: ignore

                    if self.__cog_app_commands_group__:
                        children.append(app_command)

        for command in cls.__cog_app_commands__:
            copy = command._copy_with(parent=self.__cog_app_commands_group__,
                                      binding=self)

            # Update set bindings
            if copy._attr:
                setattr(self, copy._attr, copy)

            children.append(copy)

        self.__cog_app_commands__ = children
        if self.__cog_app_commands_group__:
            self.__cog_app_commands_group__.module = cls.__module__
            mapping = {cmd.name: cmd for cmd in children}
            if len(mapping) > 25:
                raise TypeError(
                    'maximum number of application command children exceeded')

            self.__cog_app_commands_group__._children = mapping  # type: ignore  # Variance issue

        return self
Exemple #20
0
class SubscriberCog(Cog):

    subscribe_group = app_commands.Group(
        name="subscribe",
        guild_only=True,
        description="subscribe to maps, players, or members playing")
    unsubscribe_group = app_commands.Group(
        name="unsubscribe",
        guild_only=True,
        description="unsubscribe from maps, players, or members")

    def __init__(self, bot: Bot, db: Redis):
        self.bot: Bot = bot
        self.db: Redis = db

        self.long_map_names_lookup: dict[str, str] = {}
        if self.db.exists(LONG_MAP_NAMES_KEY):
            self.long_map_names_lookup = self.db.hgetall(LONG_MAP_NAMES_KEY)

        self.installed_maps: list[str] = []
        # noinspection PyProtectedMember
        if "maps_manager" in Plugin._loaded_plugins:
            # noinspection PyProtectedMember,PyUnresolvedReferences
            self.installed_maps = Plugin._loaded_plugins[
                "maps_manager"].installed_maps
            # noinspection PyProtectedMember
        elif "maps" in Plugin._loaded_plugins:
            # noinspection PyProtectedMember,PyUnresolvedReferences
            self.installed_maps = Plugin._loaded_plugins["maps"].logged_maps

        self.formatted_installed_maps: dict[str, str] = {
            mapname: mapname
            for mapname in self.installed_maps
        }
        for mapname, long_map_name in self.long_map_names_lookup.items():
            if mapname in self.installed_maps and long_map_name.lower(
            ) != mapname.lower():
                self.formatted_installed_maps[
                    mapname] = f"{long_map_name} ({mapname})"

        self.known_players: dict[int, str] = self.gather_known_players()

        self.last_notified_map: Optional[str] = None
        self.notified_steam_ids: list[int] = []

        if not self.bot.intents.presences:
            self.subscribe_group.remove_command("member")
            self.unsubscribe_group.remove_command("member")
            self.bot.remove_listener(self.on_presence_update)

        super().__init__()

    def gather_known_players(self) -> dict[int, str]:
        returned = {}
        for key in self.db.keys(LAST_USED_NAME_KEY.format("*")):
            prefix = LAST_USED_NAME_KEY.rsplit("{", maxsplit=1)[0]
            suffix = LAST_USED_NAME_KEY.rsplit("}", maxsplit=1)[-1]
            steam_id_candidate = key.replace(prefix, "").replace(suffix, "")
            if not steam_id_candidate.isdigit():
                continue

            steam_id = int(steam_id_candidate)
            last_used_name = self.db.get(key)
            returned[steam_id] = Plugin.clean_text(last_used_name)

        return returned

    @subscribe_group.command(
        name="map",
        description="Get notified when your favorite maps are played")
    @app_commands.describe(mapname="the name of the map to subscribe to")
    @app_commands.guild_only()
    async def subscribe_map(self, interaction: Interaction, mapname: str):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        stripped_mapname = mapname.strip(" ")
        if stripped_mapname == "":
            reply_embed.description = "No mapname provided."
            await interaction.edit_original_message(embed=reply_embed)
            return

        if stripped_mapname not in self.installed_maps:
            reply_embed.description = f"Map `{stripped_mapname}` is not installed."
            await interaction.edit_original_message(embed=reply_embed)
            return

        db_return_value = self.db.sadd(
            DISCORD_MAP_SUBSCRIPTION_KEY.format(interaction.user.id),
            stripped_mapname)

        if not db_return_value:
            immediate_reply_message = f"You already were subscribed to map changes for map " \
                                      f"`{self.formatted_installed_maps[stripped_mapname]}`."
        else:
            immediate_reply_message = f"You have been subscribed to map changes for map " \
                                      f"`{self.formatted_installed_maps[stripped_mapname]}`."
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_maps = self.subscribed_maps_of(interaction.user.id)
        formatted_maps = "`, `".join(
            [self.format_mapname(mapname) for mapname in subscribed_maps])
        reply_embed.description = f"{immediate_reply_message}\n" \
                                  f"You are currently subscribed to map changes for: `{formatted_maps}`"
        await interaction.edit_original_message(embed=reply_embed)

    def subscribed_maps_of(self, user_id: int) -> list[str]:
        return self.db.smembers(DISCORD_MAP_SUBSCRIPTION_KEY.format(user_id))

    def format_mapname(self, mapname: str) -> str:
        if mapname in self.formatted_installed_maps:
            return self.formatted_installed_maps[mapname]

        if mapname in self.long_map_names_lookup:
            return f"{self.long_map_names_lookup[mapname]} ({mapname})"

        return mapname

    @subscribe_map.autocomplete(name="mapname")
    async def subscribe_map_autocomplete(self, interaction: Interaction, current: str) \
            -> list[app_commands.Choice[str]]:
        subscribed_maps = self.subscribed_maps_of(interaction.user.id)
        filtered_candidates = [
            mapname for mapname, formatted_long_name in
            self.formatted_installed_maps.items()
            if current.lower() in formatted_long_name.lower()
            and mapname not in subscribed_maps
        ]
        filtered_candidates.sort()

        return [
            app_commands.Choice(name=self.formatted_installed_maps[mapname],
                                value=mapname)
            for mapname in filtered_candidates[:25]
        ]

    @subscribe_group.command(
        name="player",
        description="Get notified when your favorite players joins the server")
    @app_commands.describe(player="Name of the player you want to subscribe to"
                           )
    @app_commands.guild_only()
    async def subscribe_player(self, interaction: Interaction, player: str):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        stripped_player_name = player.strip(" ")
        if stripped_player_name == "":
            reply_embed.description = "No player name provided."
            await interaction.edit_original_message(embed=reply_embed)
            return

        matching_players = self.find_matching_players(stripped_player_name)
        if len(matching_players) == 0:
            reply_embed.description = f"No player matching player name `{stripped_player_name}` found."
            await interaction.edit_original_message(embed=reply_embed)
            return

        if len(matching_players) > 1:
            matching_player_names = [
                self.formatted_last_used_name(steam_id)
                for steam_id in matching_players
            ]
            formatted_player_names = "`, `".join(matching_player_names)
            reply_embed.description = f"More than one player matching your player name found. " \
                                      f"Players matching `{stripped_player_name}` are:\n" \
                                      f"`{formatted_player_names}`"
            await interaction.edit_original_message(embed=reply_embed)
            return

        matching_steam_id = int(matching_players[0])
        db_return_value = self.db.sadd(
            DISCORD_PLAYER_SUBSCRIPTION_KEY.format(interaction.user.id),
            matching_steam_id)

        last_used_name = self.formatted_last_used_name(matching_steam_id)
        if not db_return_value:
            immediate_reply_message = f"You already were subscribed to player `{last_used_name}`."
        else:
            immediate_reply_message = f"You have been subscribed to player `{last_used_name}`."
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_players = self.subscribed_players_of(interaction.user.id)
        formatted_players = "`, `".join([
            self.formatted_last_used_name(subscribed_steam_id)
            for subscribed_steam_id in subscribed_players
        ])
        reply_embed.description = f"{immediate_reply_message}\n" \
                                  f"You are currently subscribed to the following players: `{formatted_players}`"
        await interaction.edit_original_message(embed=reply_embed)

    def find_matching_players(self, player: str) -> list[int]:
        if player.isdigit() and int(player) in self.known_players:
            return [int(player)]

        matching_steam_ids = []
        for steam_id, player_name in self.known_players.items():
            if player.lower() in player_name.lower() or player in str(
                    steam_id):
                matching_steam_ids.append(steam_id)

        return matching_steam_ids

    def formatted_last_used_name(self, steam_id: int) -> str:
        if not self.db.exists(LAST_USED_NAME_KEY.format(steam_id)):
            return str(steam_id)
        return Plugin.clean_text(
            self.db.get(LAST_USED_NAME_KEY.format(steam_id))).replace(
                "`", r"\`")

    def subscribed_players_of(self, user_id: int) -> list[int]:
        player_subscriptions = self.db.smembers(
            DISCORD_PLAYER_SUBSCRIPTION_KEY.format(user_id))
        return [
            int(player_steam_id) for player_steam_id in player_subscriptions
        ]

    @subscribe_player.autocomplete(name="player")
    async def subscribe_player_autocomplete(self, interaction: Interaction, current: str) \
            -> list[app_commands.Choice[str]]:
        subscribed_players = self.subscribed_players_of(interaction.user.id)
        filtered_candidates = [
            candidate_steam_id
            for candidate_steam_id in self.find_matching_players(current)
            if candidate_steam_id not in subscribed_players
        ]
        filtered_candidates.sort()

        return [
            app_commands.Choice(name=self.formatted_last_used_name(steam_id),
                                value=str(steam_id))
            for steam_id in filtered_candidates[:25]
        ]

    @subscribe_group.command(
        name="member",
        description="Get notified when your favorite discord user starts playing"
    )
    @app_commands.describe(member="Discord user you want to subscribe to")
    @app_commands.guild_only()
    async def subscribe_member(self, interaction: Interaction, member: Member):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        db_return_value = self.db.sadd(
            DISCORD_MEMBER_SUBSCRIPTION_KEY.format(interaction.user.id),
            member.id)

        if not db_return_value:
            immediate_reply_message = f"You already were subscribed to Quake Live activities of {member.mention}."
        else:
            immediate_reply_message = f"You have been subscribed to Quake Live activities of {member.mention}."
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_users = self.subscribed_users_of(interaction.user.id)
        formatted_users = ", ".join(
            [user.mention for user in subscribed_users])
        reply_embed.description = f"{immediate_reply_message}\n" \
                                  f"You are currently subscribed to Quake Live activities of: {formatted_users}"
        await interaction.edit_original_message(embed=reply_embed)

    def subscribed_users_of(self, user_id: int) -> list[User]:
        subscribed_users = []
        for discord_str_id in self.db.smembers(
                DISCORD_MEMBER_SUBSCRIPTION_KEY.format(user_id)):
            discord_id = int(discord_str_id)
            subscribed_user = self.bot.get_user(discord_id)
            if subscribed_user is None:
                continue
            subscribed_users.append(subscribed_user)

        return subscribed_users

    @unsubscribe_group.command(name="map",
                               description="Stop getting notified about a map")
    @app_commands.describe(mapname="the name of the map to subscribe from")
    @app_commands.guild_only()
    async def unsubscribe_map(self, interaction: Interaction, mapname: str):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        stripped_mapname = mapname.strip(" ")
        if stripped_mapname == "":
            reply_embed.description = "No mapname provided."
            await interaction.edit_original_message(embed=reply_embed)
            return

        db_return_value = self.db.srem(
            DISCORD_MAP_SUBSCRIPTION_KEY.format(interaction.user.id),
            stripped_mapname)

        if not db_return_value:
            immediate_reply_message = f"You were not subscribed to map changes for map " \
                                      f"`{self.format_mapname(stripped_mapname)}`. "
        else:
            immediate_reply_message = f"You have been unsubscribed from map changes for map " \
                                      f"`{self.format_mapname(stripped_mapname)}`. "
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_maps = self.subscribed_maps_of(interaction.user.id)

        if len(subscribed_maps) == 0:
            reply_embed.description = f"{immediate_reply_message}\nYou are no longer subscribed to any map changes."
            await interaction.edit_original_message(embed=reply_embed)
            return

        formatted_maps = "`, `".join(
            [self.format_mapname(mapname) for mapname in subscribed_maps])
        reply_embed.description = f"{immediate_reply_message}\nYou are still subscribed to `{formatted_maps}`"
        await interaction.edit_original_message(embed=reply_embed)

    @unsubscribe_map.autocomplete("mapname")
    async def unsubscribe_map_autocomplete(self, interaction: Interaction, current: str) \
            -> list[app_commands.Choice[str]]:
        subscribed_maps = self.subscribed_maps_of(interaction.user.id)
        candidates = [
            mapname for mapname in subscribed_maps
            if current.lower() in self.format_mapname(mapname).lower()
        ]
        candidates.sort()

        return [
            app_commands.Choice(name=self.format_mapname(mapname),
                                value=mapname) for mapname in candidates[:25]
        ]

    @unsubscribe_group.command(
        name="player", description="Stop getting notified about a player")
    @app_commands.describe(
        player="Name of the player you want to unsubscribe from")
    @app_commands.guild_only()
    async def unsubscribe_player(self, interaction: Interaction, player: str):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        stripped_player_name = player.strip(" ")
        if stripped_player_name == "":
            reply_embed.description = "No player name provided."
            await interaction.edit_original_message(embed=reply_embed)
            return

        matching_players = self.find_matching_players(stripped_player_name)
        if len(matching_players) == 0:
            reply_embed.description = f"No player matching player name `{stripped_player_name}` found."
            await interaction.edit_original_message(embed=reply_embed)
            return

        if len(matching_players) > 1:
            matching_player_names = [
                self.formatted_last_used_name(steam_id)
                for steam_id in matching_players
            ]
            formatted_player_names = "`, `".join(matching_player_names)
            reply_embed.description = f"More than one player matching your player name found. " \
                                      f"Players matching `{stripped_player_name}` are:\n" \
                                      f"`{formatted_player_names}`"
            await interaction.edit_original_message(embed=reply_embed)
            return

        matching_steam_id = int(matching_players[0])
        db_return_value = self.db.srem(
            DISCORD_PLAYER_SUBSCRIPTION_KEY.format(interaction.user.id),
            stripped_player_name)

        last_used_name = self.formatted_last_used_name(matching_steam_id)
        if not db_return_value:
            immediate_reply_message = f"You were not subscribed to player `{last_used_name}`. "
        else:
            immediate_reply_message = f"You have been unsubscribed from player `{last_used_name}`. "
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_players = self.subscribed_players_of(interaction.user.id)
        formatted_players = "`, `".join([
            self.formatted_last_used_name(subscribed_steam_id)
            for subscribed_steam_id in subscribed_players
        ])

        if len(subscribed_players) == 0:
            reply_embed.description = f"{immediate_reply_message}\nYou are no longer subscribed to any players."
            await interaction.edit_original_message(embed=reply_embed)
            return

        reply_embed.description = f"{immediate_reply_message}\n" \
                                  f"You are currently subscribed to the following players: `{formatted_players}`"
        await interaction.edit_original_message(embed=reply_embed)

    @unsubscribe_player.autocomplete("player")
    async def unsubscribe_player_autocomplete(self, interaction: Interaction, current: str) \
            -> list[app_commands.Choice[str]]:
        subscribed_players = self.subscribed_players_of(interaction.user.id)

        candidates = [
            steam_id for steam_id in subscribed_players if current.lower() in
            self.formatted_last_used_name(steam_id).lower()
        ]
        candidates.sort()

        return [
            app_commands.Choice(name=self.formatted_last_used_name(steam_id),
                                value=str(steam_id))
            for steam_id in candidates[:25]
        ]

    @unsubscribe_group.command(
        name="member",
        description="Stop getting notified about a discord user")
    @app_commands.describe(member="Discord user you want to unsubscribe from")
    @app_commands.guild_only()
    async def unsubscribe_member(self, interaction: Interaction,
                                 member: Member):
        reply_embed = Embed(color=Color.blurple())
        await interaction.response.defer(thinking=True, ephemeral=True)
        db_return_value = self.db.srem(
            DISCORD_MEMBER_SUBSCRIPTION_KEY.format(interaction.user.id),
            member.id)

        if not db_return_value:
            immediate_reply_message = f"You were not subscribed to Quake Live activities of {member.mention}."
        else:
            immediate_reply_message = f"You have been unsubscribed from Quake Live activities of {member.mention}."
        reply_embed.description = immediate_reply_message
        await interaction.edit_original_message(embed=reply_embed)

        subscribed_users = self.subscribed_users_of(interaction.user.id)

        if len(subscribed_users) == 0:
            reply_embed.description = f"{immediate_reply_message}\n" \
                                      f"You are no longer subscribed to Quake Live activities of anyone."
            await interaction.edit_original_message(embed=reply_embed)
            return

        formatted_users = ", ".join(
            [user.mention for user in subscribed_users])
        reply_embed.description = f"{immediate_reply_message}\n" \
                                  f"You are still subscribed to Quake Live activities of {formatted_users}"
        await interaction.edit_original_message(embed=reply_embed)

    async def notify_map_change(self, mapname: str) -> None:
        notifications = []
        for key in self.db.keys(DISCORD_MAP_SUBSCRIPTION_KEY.format("*")):
            if self.db.sismember(key, mapname):
                prefix = DISCORD_MAP_SUBSCRIPTION_KEY.split("{", maxsplit=1)[0]
                suffix = DISCORD_MAP_SUBSCRIPTION_KEY.rsplit("}",
                                                             maxsplit=1)[-1]
                discord_id = int(key.replace(prefix, "").replace(suffix, ""))
                subscribed_discord_user = self.bot.get_user(discord_id)
                if subscribed_discord_user is None:
                    continue

                notifications.append(
                    subscribed_discord_user.send(
                        content=
                        f"`{self.format_mapname(mapname)}`, one of your favourite maps has been loaded!"
                    ))

        await asyncio.gather(*notifications)

    async def notify_player_connected(self, player: Player) -> None:
        notifications = []
        for key in self.db.keys(DISCORD_PLAYER_SUBSCRIPTION_KEY.format("*")):
            if self.db.sismember(key, player.steam_id):
                prefix = DISCORD_PLAYER_SUBSCRIPTION_KEY.split("{",
                                                               maxsplit=1)[0]
                suffix = DISCORD_PLAYER_SUBSCRIPTION_KEY.rsplit("}",
                                                                maxsplit=1)[-1]
                discord_id = int(key.replace(prefix, "").replace(suffix, ""))
                subscribed_discord_user = self.bot.get_user(discord_id)

                if subscribed_discord_user is None:
                    continue

                notifications.append(
                    subscribed_discord_user.send(
                        content=
                        f"`{player.clean_name}`, one of your followed players, "
                        f"just connected to the server!"))

        await asyncio.gather(*notifications)

    async def check_subscriptions(self):
        notification_actions = []
        game = None
        try:
            game = minqlx.Game()
        except minqlx.NonexistentGameError:
            pass

        if game is not None and game.map != self.last_notified_map:
            self.last_notified_map = game.map
            notification_actions.append(
                self.notify_map_change(self.last_notified_map))

        players = Plugin.players()
        new_players = [
            player for player in players
            if player.steam_id not in self.notified_steam_ids
        ]
        for player in new_players:
            notification_actions.append(self.notify_player_connected(player))
        self.notified_steam_ids = [player.steam_id for player in players]
        await asyncio.gather(*notification_actions)

    # noinspection PyMethodMayBeStatic
    def find_relevant_activity(self, member: Member) -> Optional[Activity]:
        for activity in member.activities:
            if activity.type != ActivityType.playing:
                continue
            if activity.name is None or "Quake Live" not in activity.name:
                continue
            return activity

        return None

    @GroupCog.listener()
    async def on_presence_update(self, before: Member, after: Member):
        relevant_activity = self.find_relevant_activity(before)
        if relevant_activity is not None:
            return

        relevant_activity = self.find_relevant_activity(after)
        if relevant_activity is None:
            return

        notifications = []
        for key in self.db.keys(DISCORD_MEMBER_SUBSCRIPTION_KEY.format("*")):
            if self.db.sismember(key, str(after.id)):
                prefix = DISCORD_MEMBER_SUBSCRIPTION_KEY.split("{",
                                                               maxsplit=1)[0]
                suffix = DISCORD_MEMBER_SUBSCRIPTION_KEY.rsplit("}",
                                                                maxsplit=1)[-1]
                discord_id = int(key.replace(prefix, "").replace(suffix, ""))
                informed_user = self.bot.get_user(discord_id)
                if informed_user is None:
                    continue

                notifications.append(
                    informed_user.send(
                        content=
                        f"{after.display_name}, a discord user you are subscribed to, "
                        f"just started playing Quake Live."))

        await asyncio.gather(*notifications)