Exemple #1
0
class ConfigCog(commands.Cog):
    def __init__(self, bot: SpellBot):
        self.bot = bot

    @app_commands.command(
        name="power",
        description="Set your power level.",
    )
    @app_commands.describe(level="What is your current power level?")
    @app_commands.choices(
        level=[
            Choice(name="1", value=1),
            Choice(name="2", value=2),
            Choice(name="3", value=3),
            Choice(name="4", value=4),
            Choice(name="5", value=5),
            Choice(name="6", value=6),
            Choice(name="7", value=7),
            Choice(name="8", value=8),
            Choice(name="9", value=9),
            Choice(name="10", value=10),
        ], )
    @tracer.wrap(name="interaction", resource="power")
    async def power(self,
                    interaction: discord.Interaction,
                    level: Optional[int] = None) -> None:
        assert interaction.channel_id is not None
        add_span_context(interaction)
        async with self.bot.channel_lock(interaction.channel_id):
            async with ConfigAction.create(self.bot, interaction) as action:
                await action.power(level=level)
Exemple #2
0
 async def event_name_autocomplete(self, interaction: discord.Interaction,
                                   current):
     if not current:
         return []
     current = current.lower()
     names = set()
     timers = self.gamedata["event_timers"]
     for category in timers:
         if category == "bosses":
             subtypes = "normal", "hardcore"
             for subtype in subtypes:
                 for boss in timers[category][subtype]:
                     name = boss["name"]
                     if current in name.lower():
                         names.add(Choice(name=name, value=name))
         else:
             for subcategory in timers[category]:
                 map_name = subcategory["name"]
                 for event in subcategory["phases"]:
                     if event["name"]:
                         name = f"{map_name} - {event['name']}"
                         if current in name.lower():
                             names.add(
                                 Choice(name=name, value=event["name"]))
     return sorted(list(names)[:25], key=lambda c: c.name)
Exemple #3
0
    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
Exemple #4
0
    async def item_autocomplete(self, interaction: discord.Interaction,
                                current: str):
        if not current:
            return []

        def consolidate_duplicates(items):
            unique_items = collections.OrderedDict()
            for item in items:
                item_tuple = item["name"], item["rarity"], item["type"]
                if item_tuple not in unique_items:
                    unique_items[item_tuple] = []
                unique_items[item_tuple].append(item["_id"])
            unique_list = []
            for k, v in unique_items.items():
                ids = " ".join(str(i) for i in v)
                if len(ids) > 100:
                    continue
                unique_list.append({
                    "name": k[0],
                    "rarity": k[1],
                    "ids": ids,
                    "type": k[2]
                })
            return unique_list

        query = prepare_search(current)
        query = {
            "name": query,
        }
        items = await self.db.items.find(query).to_list(25)
        items = sorted(consolidate_duplicates(items), key=lambda c: c["name"])
        return [
            Choice(name=f"{it['name']} - {it['rarity']}", value=it["ids"])
            for it in items
        ]
Exemple #5
0
 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]
Exemple #6
0
 async def chatcode_upgrade_autocomplete(self,
                                         interaction: discord.Interaction,
                                         current: str):
     if not current:
         return []
     query = prepare_search(current)
     query = {"name": query, "type": "UpgradeComponent"}
     items = await self.db.items.find(query).to_list(25)
     return [Choice(name=it["name"], value=str(it["_id"])) for it in items]
Exemple #7
0
 async def currency_autocomplete(self, interaction: discord.Interaction,
                                 current: str):
     if not current:
         return []
     if current == "gold":
         current = "coin"
     query = prepare_search(current)
     query = {"name": query}
     items = await self.db.currencies.find(query).to_list(25)
     return [Choice(name=it["name"], value=str(it["_id"])) for it in items]
Exemple #8
0
 async def skill_autocomplete(self,
                                      interaction: discord.Interaction,
                                      current: str):
     if not current:
         return []
     query = prepare_search(current)
     query = {
         "name": query,  "professions": {"$ne": None}
     }
     items = await self.db.skills.find(query).to_list(25)
     return [Choice(name=it["name"], value=str(it["_id"])) for it in items]
Exemple #9
0
    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
Exemple #10
0
 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()]
Exemple #11
0
 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]
Exemple #12
0
class LookingForGameCog(commands.Cog):
    def __init__(self, bot: SpellBot):
        self.bot = bot

    @app_commands.command(name="lfg", description="Looking for game.")
    @app_commands.describe(friends="Mention friends to join this game with.")
    @app_commands.describe(seats="How many players can be seated at this game?"
                           )
    @app_commands.choices(
        seats=[
            Choice(name="2", value=2),
            Choice(name="3", value=3),
            Choice(name="4", value=4),
        ], )
    @app_commands.describe(format="What game format do you want to play?")
    @app_commands.choices(
        format=[
            Choice(name=str(format), value=format.value)
            for format in GameFormat
        ], )
    @tracer.wrap(name="interaction", resource="lfg")
    async def lfg(
        self,
        interaction: discord.Interaction,
        friends: Optional[str] = None,
        seats: Optional[int] = None,
        format: Optional[int] = None,
    ):
        assert interaction.channel_id is not None
        add_span_context(interaction)
        await interaction.response.defer()
        async with self.bot.channel_lock(interaction.channel_id):
            async with LookingForGameAction.create(self.bot,
                                                   interaction) as action:
                await action.execute(friends=friends,
                                     seats=seats,
                                     format=format)
Exemple #13
0
class EventsCog(commands.Cog):
    def __init__(self, bot: SpellBot):
        self.bot = bot

    @app_commands.command(name="game", description="Immediately create and start an ad-hoc game.")
    @app_commands.describe(players="Mention all players for this game.")
    @app_commands.describe(format="What game format? Default if unspecified is Commander.")
    @app_commands.choices(
        format=[Choice(name=str(format), value=format.value) for format in GameFormat],
    )
    @tracer.wrap(name="interaction", resource="game")
    async def game(
        self,
        interaction: discord.Interaction,
        players: str,
        format: Optional[int] = None,
    ) -> None:
        add_span_context(interaction)
        async with LookingForGameAction.create(self.bot, interaction) as action:
            await action.create_game(players, format)
Exemple #14
0
class DailyMixin:
    @app_commands.command()
    @app_commands.describe(category="Daily type",
                           tomorrow="Show tomorrow's dailies instead")
    @app_commands.choices(category=[
        Choice(**cat) for cat in [{
            "name": "All",
            "value": "all"
        }] + DAILY_CATEGORIES
    ])
    async def daily(self,
                    interaction: discord.Interaction,
                    category: str,
                    tomorrow: bool = False):
        """Show today's daily achievements"""
        await interaction.response.defer()
        tomorrow = bool(tomorrow)
        if category == "all":
            category = ["psna", "pve", "pvp", "wvw", "fractals", "strikes"]
        else:
            category = [category]
        embed = await self.daily_embed(category,
                                       interaction=interaction,
                                       tomorrow=tomorrow)
        await interaction.followup.send(embed=embed)

    def get_year_day(self, tomorrow=True):
        date = datetime.datetime.utcnow().date()
        if tomorrow:
            date += datetime.timedelta(days=1)
        day = (date - date.replace(month=1, day=1)).days
        if day >= 59 and not calendar.isleap(date.year):
            day += 1
        return day

    async def daily_embed(self,
                          categories,
                          *,
                          doc=None,
                          interaction=None,
                          tomorrow=False):
        # All of this mess needs a rewrite at this point tbh, but I just keep
        # adding more on top of it. Oh well, it works.. for now
        if not doc:
            doc = await self.bot.database.get_cog_config(self)
        if interaction:
            color = await self.get_embed_color(interaction)
        else:
            color = self.embed_color
        embed = discord.Embed(title="Dailies", color=color)
        if tomorrow:
            embed.title += " tomorrow"
        key = "dailies" if not tomorrow else "dailies_tomorrow"
        dailies = doc["cache"][key]
        for category in categories:
            if category == "psna":
                if datetime.datetime.utcnow().hour >= 8:
                    value = "\n".join(dailies["psna_later"])
                else:
                    value = "\n".join(dailies["psna"])
            elif category == "fractals":
                fractals = self.get_fractals(dailies["fractals"],
                                             interaction,
                                             tomorrow=tomorrow)
                value = "\n".join(fractals[0])
            elif category == "strikes":
                category = "Priority Strike"
                strikes = self.get_strike(interaction, tomorrow=tomorrow)
                value = strikes
            else:
                lines = []
                for i, d in enumerate(dailies[category]):
                    # HACK handling for emojis for lws dailies. Needs rewrite
                    emoji = self.get_emoji(interaction, f"daily {category}")
                    if category == "pve":
                        if i == 5:
                            emoji = self.get_emoji(interaction, "daily lws3")
                        elif i == 6:
                            emoji = self.get_emoji(interaction, "daily lws4")
                    lines.append(emoji + d)
                value = "\n".join(lines)
            if category == "psna_later":
                category = "psna in 8 hours"
            value = re.sub(r"(?:Daily|Tier 4|PvP|WvW) ", "", value)
            if category.startswith("psna"):
                category = self.get_emoji(interaction, "daily psna") + category
            if category == "fractals":
                embed.add_field(name="> Daily Fractals",
                                value="\n".join(fractals[0]))
                embed.add_field(name="> CM Instabilities", value=fractals[2])
                embed.add_field(
                    name="> Recommended Fractals",
                    value="\n".join(fractals[1]),
                    inline=False,
                )

            else:
                embed.add_field(name=category.upper(),
                                value=value,
                                inline=False)
        if "fractals" in categories:
            embed.set_footer(
                text=self.bot.user.name +
                " | Instabilities shown only apply to the highest scale",
                icon_url=self.bot.user.display_avatar.url,
            )
        else:
            embed.set_footer(text=self.bot.user.name,
                             icon_url=self.bot.user.display_avatar.url)
        embed.timestamp = datetime.datetime.utcnow()
        return embed

    def get_lw_dailies(self, tomorrow=False):
        LWS3_MAPS = [
            "Bloodstone Fen",
            "Ember Bay",
            "Bitterfrost Frontier",
            "Lake Doric",
            "Draconis Mons",
            "Siren's Landing",
        ]
        LWS4_MAPS = [
            "Domain of Istan",
            "Sandswept Isles",
            "Domain of Kourna",
            "Jahai Bluffs",
            "Thunderhead Peaks",
            "Dragonfall",
        ]
        day = self.get_year_day(tomorrow=tomorrow)
        index = day % (len(LWS3_MAPS))
        lines = []
        lines.append(f"Daily Living World Season 3 - {LWS3_MAPS[index]}")
        lines.append(f"Daily Living World Season 4 - {LWS4_MAPS[index]}")
        return lines

    def get_fractals(self, fractals, ctx, tomorrow=False):
        recommended_fractals = []
        daily_fractals = []
        fractals_data = self.gamedata["fractals"]
        for fractal in fractals:
            fractal_level = fractal.replace("Daily Recommended Fractal—Scale ",
                                            "")
            if re.match("[0-9]{1,3}", fractal_level):
                recommended_fractals.append(fractal_level)
            else:
                line = self.get_emoji(ctx, "daily fractal") + re.sub(
                    r"(?:Daily|Tier 4) ", "", fractal)
                try:
                    scale = self.gamedata["fractals"][fractal[13:]][-1]
                    instabilities = self.get_instabilities(scale,
                                                           ctx=ctx,
                                                           tomorrow=tomorrow)
                    if instabilities:
                        line += f"\n{instabilities}"
                except (IndexError, KeyError):
                    pass
                daily_fractals.append(line)
        for i, level in enumerate(sorted(recommended_fractals, key=int)):
            for k, v in fractals_data.items():
                if int(level) in v:
                    recommended_fractals[i] = "{}{} {}".format(
                        self.get_emoji(ctx, "daily recommended fractal"),
                        level, k)

        return (
            daily_fractals,
            recommended_fractals,
            self.get_cm_instabilities(ctx=ctx, tomorrow=tomorrow),
        )

    def get_psna(self, *, offset_days=0):
        offset = datetime.timedelta(hours=-8)
        tzone = datetime.timezone(offset)
        day = datetime.datetime.now(tzone).weekday()
        if day + offset_days > 6:
            offset_days = -6
        return self.gamedata["pact_supply"][day + offset_days]

    def get_strike(self, ctx, tomorrow=False):
        day = self.get_year_day(tomorrow=tomorrow)
        index = day % len(self.gamedata["strike_missions"])
        return (self.get_emoji(ctx, "daily strike") +
                self.gamedata["strike_missions"][index])

    def get_instabilities(self, fractal_level, *, tomorrow=False, ctx=None):
        fractal_level = str(fractal_level)
        day = self.get_year_day(tomorrow=tomorrow)
        if fractal_level not in self.instabilities["instabilities"]:
            return None
        levels = self.instabilities["instabilities"][fractal_level][day]
        names = []
        for instab in levels:
            name = self.instabilities["instability_names"][instab]
            if ctx:
                name = (en_space + tab +
                        self.get_emoji(ctx, name.replace(",", "")) + name)
            names.append(name)
        return "\n".join(names)

    def get_cm_instabilities(self, *, ctx=None, tomorrow=False):
        cm_instabs = []
        cm_fractals = "Nightmare", "Shattered Observatory", "Sunqua Peak"
        for fractal in cm_fractals:
            scale = self.gamedata["fractals"][fractal][-1]
            line = self.get_emoji(ctx, "daily fractal") + fractal
            instabilities = self.get_instabilities(scale,
                                                   ctx=ctx,
                                                   tomorrow=tomorrow)
            cm_instabs.append(line + "\n" + instabilities)
        return "\n".join(cm_instabs)
Exemple #15
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 #16
0
class MiscMixin:
    @app_commands.command()
    @app_commands.describe(
        language="The language of the wiki to search on. Optional. "
        "Defaults to English.",
        search_text="The text to search the wiki for. Example: Lion's Arch")
    @app_commands.choices(language=[
        Choice(name=p.title(), value=p) for p in ["en", "fr", "es", "de"]
    ])
    async def wiki(
            self,
            interaction: discord.Interaction,
            search_text: str,  # TODO autocomplete
            language: str = "en"):
        """Search the Guild wars 2 wiki"""
        if len(search_text) > 300:
            return await interaction.response.send_message("Search too long",
                                                           ephemeral=True)
        await interaction.response.defer()
        wiki = {
            "en": "https://wiki.guildwars2.com",
            "de": "https://wiki-de.guildwars2.com",
            "fr": "https://wiki-fr.guildwars2.com",
            "es": "https://wiki-es.guildwars2.com"
        }
        search_url = {
            "en": "{}/index.php?title=Special%3ASearch&search={}",
            "de": "{}/index.php?search={}&title=Spezial%3ASuche&",
            "fr": "{}/index.php?search={}&title=Spécial%3ARecherche",
            "es": "{}/index.php?title=Especial%3ABuscar&search={}"
        }
        url = (search_url[language].format(wiki[language], search_text))
        headers = {"User-Agent": "TybaltBot/v2"}
        # Overzealous filtering on the wiki's side lead to the bot's IP being blocked.
        # Seems to be a common issue, based on https://wiki.guildwars2.com/wiki/Guild_Wars_2_Wiki:Reporting_wiki_bugs#Forbidden_403
        # And based on the information within, the wiki had added an exemption for requests with this user-string header
        # It is just a little dirty, but, it doesn't really change anything in the end.
        # The only thing being checked is this user-string, and
        # given the lack of any other verification, I don't think it's anything too bad.
        # That being said, if anyone takes an issue with this, I will contact the wiki
        # and get an exemption for GW2bot too.
        async with self.session.get(url, headers=headers) as r:
            if r.history:  # Redirected
                embed = await self.search_results_embed(interaction,
                                                        "Wiki",
                                                        exact_match=r)
                return await interaction.followup.send(embed=embed)
            else:
                results = await r.text()
                soup = BeautifulSoup(results, 'html.parser')
                posts = soup.find_all(
                    "div", {"class": "mw-search-result-heading"})[:5]
                if not posts:
                    return await interaction.followup.send(
                        "No results for your search")
        embed = await self.search_results_embed(interaction,
                                                "Wiki",
                                                posts,
                                                base_url=wiki[language])
        await interaction.followup.send(embed=embed)

    async def search_results_embed(self,
                                   ctx,
                                   site,
                                   posts=None,
                                   *,
                                   base_url="",
                                   exact_match=None):
        if exact_match:
            soup = BeautifulSoup(await exact_match.text(), 'html.parser')
            embed = discord.Embed(title=soup.title.get_text(),
                                  color=await self.get_embed_color(ctx),
                                  url=str(exact_match.url))
            return embed
        embed = discord.Embed(title="{} search results".format(site),
                              description="Closest matches",
                              color=await self.get_embed_color(ctx))
        for post in posts:
            post = post.a
            url = base_url + post['href']
            url = url.replace(")", "\\)")
            embed.add_field(name=post["title"],
                            value="[Click here]({})".format(url),
                            inline=False)
        return embed

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

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

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

    @app_commands.command()
    @app_commands.describe(
        item="Base item name for the chat code. Example: Banana",
        quantity="Item quantity, ranging from 1 to 255.",
        skin="Skin name to apply on the item.",
        upgrade_1="Name of the upgrade in the first slot. "
        "Example: Mark of Penetration",
        upgrade_2="Name of the upgrade in the second slot. "
        "Example: Superior rune of Generosity")
    @app_commands.autocomplete(item=chatcode_item_autocomplete,
                               skin=chatcode_skin_autocomplete,
                               upgrade_1=chatcode_upgrade_autocomplete,
                               upgrade_2=chatcode_upgrade_autocomplete)
    async def chatcode(
        self,
        interaction: discord.Interaction,
        item: str,
        quantity: int,
        skin: str = None,
        upgrade_1: str = None,
        upgrade_2: str = None,
    ):
        """Generate a chat code"""
        if not 1 <= quantity <= 255:
            return await interaction.response.send_message(
                "Invalid quantity. Quantity can be a number between 1 and 255",
                ephemeral=True)
        try:
            item = int(item)
            skin = int(skin) if skin else None
            upgrade_1 = int(upgrade_1) if upgrade_1 else None
            upgrade_2 = int(upgrade_2) if upgrade_2 else None
        except ValueError:
            return await interaction.response.send_message("Invalid value",
                                                           ephemeral=True)
        upgrades = []
        if upgrade_1:
            upgrades.append(upgrade_1)
        if upgrade_2:
            upgrades.append(upgrade_2)
        chat_code = self.generate_chat_code(item, quantity, skin, upgrades)
        output = "Here's your chatcode. No refunds. ```\n{}```".format(
            chat_code)
        await interaction.response.send_message(output, ephemeral=True)

    def generate_chat_code(self, item_id, count, skin_id, upgrades):
        def little_endian(_id):
            return [int(x) for x in struct.pack("<i", _id)]

        def upgrade_flag():
            skin = 0
            first = 0
            second = 0
            if skin_id:
                skin = 128
            if len(upgrades) == 1:
                first = 64
            if len(upgrades) == 2:
                second = 32
            return skin | first | second

        link = [2, count]
        link.extend(little_endian(item_id))
        link = link[:5]
        link.append(upgrade_flag())
        for x in filter(None, (skin_id, *upgrades)):
            link.extend(little_endian(x))
        link.append(0)
        output = codecs.encode(bytes(link), 'base64').decode('utf-8')
        return "[&{}]".format(output.strip())
Exemple #17
0
class CustomCommands(commands.Cog):
    def __init__(self, client):
        self.client = client

    @commands.Cog.listener()
    async def on_ready(self):
        db.execute(
            'CREATE TABLE IF NOT EXISTS custom_commands (command text unique, message text, level text)'
        )
        connection.commit()
        print('Custom Command Module Loaded.')

    @commands.Cog.listener()
    async def on_message(self, message):
        command = message.content.split(' ')[0]
        if not isinstance(message.channel, discord.DMChannel):
            if await is_command(command) and not message.author.bot:

                print(
                    f'{message.author}({message.author.id}) executed custom command: {command}.'
                )

                level = await get_level(command)

                if level == '-b':
                    if await helpers.role_helper.is_role_defined('booster'):
                        if await helpers.role_helper.has_role(
                                message.guild, message.author.id, 'booster'
                        ) or message.author.guild_permissions.administrator:
                            await send_response(message.channel, await
                                                get_response(command),
                                                message.author)
                elif level == '-s':
                    if await helpers.role_helper.is_role_defined('sub'):
                        if await helpers.role_helper.has_role(
                                message.guild, message.author.id, 'sub'
                        ) or message.author.guild_permissions.administrator:
                            await send_response(message.channel, await
                                                get_response(command),
                                                message.author)
                elif level == '-m':
                    if await helpers.role_helper.is_role_defined('mod'):
                        if await helpers.role_helper.has_role(
                                message.guild, message.author.id, 'mod'
                        ) or message.author.guild_permissions.administrator:
                            await send_response(message.channel, await
                                                get_response(command),
                                                message.author)
                elif level == '-a':
                    await send_response(message.channel, await
                                        get_response(command), message.author)
                else:
                    if str(
                            message.author.id
                    ) == level or message.author.guild_permissions.administrator:
                        await send_response(message.channel, await
                                            get_response(command),
                                            message.author)

    valid_levels = ['-a', '-b', '-s', '-m']
    level_choices = []

    for lvl in valid_levels:
        level_choices.append(Choice(name=get_level_string(lvl), value=lvl))

    @app_commands.command(
        name='command',
        description='Create a custom command to use in the server')
    @app_commands.choices(level=level_choices)
    async def custom_command(self, interaction: discord.Interaction,
                             command_name: str, level: str, response: str):
        if await helpers.role_helper.has_role(
                interaction.guild, interaction.user.id,
                'mod') or interaction.user.guild_permissions.administrator:
            print(
                f'{interaction.user}({interaction.user.id}) executed Command command.'
            )

            if not await is_command(command_name):
                if level in self.valid_levels:
                    await add_command(command_name, response, level)
                    await interaction.response.send_message(
                        embed=await helpers.embed_helper.create_success_embed(
                            f'Command `{command_name}` created successfully.',
                            self.client.guilds[0].get_member(
                                self.client.user.id).color))
        else:
            await interaction.response.send_message(
                embed=await helpers.embed_helper.create_error_embed(
                    'You do not have permission to use this command.'))

    @app_commands.command(
        name='delete', description='Delete a custom command from the server.')
    async def delete(self, interaction: discord.Interaction,
                     command_name: str):
        if await helpers.role_helper.has_role(
                interaction.guild, interaction.user.id,
                'mod') or interaction.user.guild_permissions.administrator:
            print(
                f'{interaction.user}({interaction.user.id}) executed Delete command.'
            )

            if await is_command(command_name):
                await remove_command(command_name)
                await interaction.response.send_message(
                    embed=await helpers.embed_helper.create_success_embed(
                        f'Command `{command_name}` deleted successfully.',
                        self.client.guilds[0].get_member(
                            self.client.user.id).color))
            else:
                await interaction.response.send_message(
                    embed=await helpers.embed_helper.create_error_embed(
                        f'Command `{command_name}` does not exist.'))
        else:
            await interaction.response.send_message(
                embed=await helpers.embed_helper.create_error_embed(
                    'You do not have permission to use this command.'))

    @app_commands.command(
        name='commands',
        description='Shows list of the custom commands on the server.')
    async def custom_commands(self, interaction: discord.Interaction):

        print(
            f'{interaction.user}({interaction.user.id}) executed Commands command.'
        )

        embeds = []
        command_list = await get_commands()
        commands_per_page = 6

        total_pages = math.ceil(len(command_list) / commands_per_page)

        for j in range(1, total_pages + 1):
            embed = discord.Embed(title='Custom Commands',
                                  color=self.client.guilds[0].get_member(
                                      self.client.user.id).color)
            number = min((commands_per_page * j), len(command_list))

            for i in range((commands_per_page * (j - 1)), number):
                if i == commands_per_page * (j - 1):
                    embed.add_field(name='Command',
                                    value=command_list[i][0],
                                    inline=True)
                    embed.add_field(name='Response',
                                    value=command_list[i][1],
                                    inline=True)
                    embed.add_field(name='Permission',
                                    value=get_level_string(command_list[i][2]),
                                    inline=True)
                else:
                    embed.add_field(name='\u200b',
                                    value=command_list[i][0],
                                    inline=True)
                    embed.add_field(name='\u200b',
                                    value=command_list[i][1],
                                    inline=True)
                    embed.add_field(name='\u200b',
                                    value=get_level_string(command_list[i][2]),
                                    inline=True)
            embed.add_field(name='\u200b',
                            value=f'Page [{j}/{total_pages}]',
                            inline=True)

            embeds.append(embed)

        view = CommandsView(embeds, total_pages)

        await interaction.response.send_message(embed=embeds[0], view=view)
class Birthday(commands.Cog, name="birthday"):
	"""
		Set your birthday, and when the time comes I will wish you a happy birthday !
		
		Require intents: 
			- default
		
		Require bot permission:
			- None
	"""
	def __init__(self, bot: commands.Bot) -> None:
		self.bot = bot

		self.birthday_data = bot.config["database"]["birthday"]

	def help_custom(self) -> tuple[str]:
		emoji = '🎁'
		label = "Birthday"
		description = "Maybe I'll wish you soon a happy birthday !"
		return emoji, label, description

	async def cog_load(self):
		self.daily_birthday.start()

	async def cog_unload(self):
		self.daily_birthday.cancel()

	@tasks.loop(hours=1)
	async def daily_birthday(self):
		if datetime.now().hour == 9:
			guild = get(self.bot.guilds, id=self.birthday_data["guild_id"])
			channel = get(guild.channels, id=self.birthday_data["channel_id"])

			response = await self.bot.database.select(self.birthday_data["table"], "*")
			for data in response:
				user_id, user_birth = data

				if user_birth.month == datetime.now().month and user_birth.day == datetime.now().day:
					timestamp = round(time.mktime(user_birth.timetuple()))

					message = f"Remember this date because it's <@{user_id}>'s birthday !\nHe was born <t:{timestamp}:R> !"
					images = [
						"https://sayingimages.com/wp-content/uploads/funny-birthday-and-believe-me-memes.jpg",
						"https://i.kym-cdn.com/photos/images/newsfeed/001/988/649/1e8.jpg",
						"https://winkgo.com/wp-content/uploads/2018/08/101-Best-Happy-Birthday-Memes-01-720x720.jpg",
						"https://www.the-best-wishes.com/wp-content/uploads/2022/01/success-kid-cute-birthday-meme-for-her.jpg"
					]

					embed = discord.Embed(title="🎉 Happy birthday !", description=message, colour=discord.Colour.dark_gold())
					embed.set_image(url=images[random.randint(0, len(images)-1)])
					await channel.send(embed=embed)

	@daily_birthday.before_loop
	async def before_daily_birthday(self):
		await self.bot.wait_until_ready()
		while self.bot.database.connector is None: await asyncio.sleep(0.01) #wait_for initBirthday

	async def year_suggest(self, _: discord.Interaction, current: str):
		years = [str(i) for i in range(datetime.now().year - 99, datetime.now().year - 15)]
		if not current: 
			out = [app_commands.Choice(name=i, value=i) for i in range(datetime.now().year - 30, datetime.now().year - 15)]
		else:
			out = [app_commands.Choice(name=year, value=int(year)) for year in years if str(current) in year]
		if len(out) > 25:
			return out[:25]
		else:
			return out

	async def day_suggest(self, _: discord.Interaction, current: str):
		days = [str(i) for i in range(1, 32)]
		if not current:
			out = [app_commands.Choice(name=i, value=i) for i in range(1, 16)]
		else:
			out = [app_commands.Choice(name=day, value=int(day)) for day in days if str(current) in day]
		if len(out) > 25:
			return out[:25]
		else:
			return out

	@app_commands.command(name="birthday", description="Set your own birthday.")
	@app_commands.describe(month="Your month of birth.", day="Your day of birth.", year="Your year of birth.")
	@app_commands.choices(month=[Choice(name=datetime(1, i, 1).strftime("%B"), value=i) for i in range(1, 13)])
	@app_commands.autocomplete(day=day_suggest, year=year_suggest)
	@app_commands.checks.has_permissions(use_slash_commands=True)
	@app_commands.checks.cooldown(1, 15.0, key=lambda i: (i.guild_id, i.user.id))
	async def birthday(self, interaction: discord.Interaction, month: int, day: int, year: int):
		"""Allows you to set/show your birthday."""
		if day > 31 or day < 0 or year > datetime.now().year - 15 or year < datetime.now().year - 99:
			raise ValueError("Please provide a real date of birth.")

		try:
			dataDate = datetime.strptime(f"{day}{month}{year}", "%d%m%Y").date()
			if dataDate.year > datetime.now().year - 15 or dataDate.year < datetime.now().year - 99: 
				raise commands.CommandError("Please provide your real year of birth.")
			# Insert
			await self.bot.database.insert(self.birthday_data["table"], {"user_id": interaction.user.id, "user_birth": dataDate})
			# Update
			await self.bot.database.update(self.birthday_data["table"], "user_birth", dataDate, f"user_id = {interaction.user.id}")

			await self.show_birthday_message(interaction, interaction.user)
		except Exception as e:
			raise commands.CommandError(str(e))

	@app_commands.command(name="showbirthday", description="Display the birthday of a user.")
	@app_commands.describe(user="******")
	@app_commands.checks.cooldown(1, 10.0, key=lambda i: (i.guild_id, i.user.id))
	async def show_birthday(self, interaction: discord.Interaction, user: discord.Member = None):
		"""Allows you to show the birthday of other users."""
		if not user: 
			user = interaction.user
		await self.show_birthday_message(interaction, user)

	async def show_birthday_message(self, interaction: discord.Interaction, user:discord.Member) -> None:
		response = await self.bot.database.lookup(self.birthday_data["table"], "user_birth", "user_id", str(user.id))
		if response:
			dataDate : date = response[0][0]
			timestamp = round(time.mktime(dataDate.timetuple()))
			await interaction.response.send_message(f":birthday: Birthday the <t:{timestamp}:D> and was born <t:{timestamp}:R>.")
		else:
			await interaction.response.send_message(":birthday: Nothing was found. Set the birthday and retry.")
Exemple #19
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 #20
0
class EventsMixin:

    @app_commands.command()
    @app_commands.describe(category="Event timer category")
    @app_commands.choices(category=[Choice(**c) for c in ET_CATEGORIES])
    async def et(self, interaction: discord.Interaction, category: str):
        """Event timer"""
        if category == "bosses":
            embed = self.schedule_embed()
        else:
            embed = await self.timer_embed(interaction, category)
        await interaction.response.send_message(embed=embed)

    async def event_name_autocomplete(self, interaction: discord.Interaction,
                                      current):
        if not current:
            return []
        current = current.lower()
        names = set()
        timers = self.gamedata["event_timers"]
        for category in timers:
            if category == "bosses":
                subtypes = "normal", "hardcore"
                for subtype in subtypes:
                    for boss in timers[category][subtype]:
                        name = boss["name"]
                        if current in name.lower():
                            names.add(Choice(name=name, value=name))
            else:
                for subcategory in timers[category]:
                    map_name = subcategory["name"]
                    for event in subcategory["phases"]:
                        if event["name"]:
                            name = f"{map_name} - {event['name']}"
                            if current in name.lower():
                                names.add(
                                    Choice(name=name, value=event["name"]))
        return sorted(list(names)[:25], key=lambda c: c.name)

    @app_commands.command()
    @app_commands.describe(
        event_name="Event name. Examples: Shadow Behemoth. Gerent Preparation",
        minutes_before_event="The number of minutes before "
        "the event that you'll be notified at")
    @app_commands.autocomplete(event_name=event_name_autocomplete)
    async def event_reminder(self,
                             interaction: discord.Interaction,
                             event_name: str,
                             minutes_before_event: int = 5):
        """Make the bot automatically notify you before an event starts"""
        if minutes_before_event < 0:
            return await interaction.response.send_message(
                "That's not how time works!", ephemeral=True)
        if minutes_before_event > 60:
            return await interaction.response.send_message(
                "Time can't be greater than one hour", ephemeral=True)
        event_name = event_name.lower()
        reminder = {}
        for boss in self.boss_schedule:
            if boss["name"].lower() == event_name:
                reminder["type"] = "boss"
                reminder["name"] = boss["name"]
        if not reminder:
            for group in "hot", "pof", "day", "ibs", "eod":
                maps = self.gamedata["event_timers"][group]
                for location in maps:
                    for phase in location["phases"]:
                        if not phase["name"]:
                            continue
                        if phase["name"].lower() == event_name:
                            reminder["type"] = "phase"
                            reminder["name"] = phase["name"]
                            reminder["group"] = group
                            reminder["map_name"] = location["name"]
        if not reminder:
            return await interaction.response.send_message(
                "No event found matching that name", ephemeral=True)
        reminder["time"] = minutes_before_event * 60
        await self.bot.database.set(interaction.user,
                                    {"event_reminders": reminder},
                                    self,
                                    operator="push")
        await interaction.response.send_message("Reminder set succesfully",
                                                ephemeral=True)

    async def et_reminder_settings_menu(self, ctx):
        # Unimplemented. Should get around to it sometime.
        user = ctx.author
        embed_templates = [
            {
                "setting":
                "online_only",
                "title":
                "Online only",
                "description":
                "Enable to have reminders sent only when you're online on Discord",
                "footer":
                "Note that the bot can't distinguish whether you're invisible or offline",
            },
            {
                "setting":
                "ingame_only",
                "title":
                "Ingame only",
                "description":
                "Enable to have reminders sent only while you're in game",
                "footer":
                "This works based off your Discord game status. Make sure to enable it",
            },
        ]
        doc = await self.bot.database.get(user, self)
        doc = doc.get("et_reminder_settings", {})
        settings = [t["setting"] for t in embed_templates]
        settings = {s: doc.get(s, False) for s in settings}
        messages = []
        reactions = {"✔": True, "❌": False}
        to_cleanup = [
            await user.send("Use reactions below to configure reminders")
        ]

        def setting_embed(template):
            enabled = "enabled" if settings[
                template["setting"]] else "disabled"
            description = (f"**{template['description']}**\n"
                           f"Current state: **{enabled}**")
            embed = discord.Embed(title=template["title"],
                                  description=description,
                                  color=self.embed_color)
            if template["footer"]:
                embed.set_footer(text=template["footer"])
            return embed

        for template in embed_templates:
            embed = setting_embed(template)
            msg = await user.send(embed=embed)
            messages.append({"message": msg, "setting": template["setting"]})
            to_cleanup.append(msg)
            for reaction in reactions:
                asyncio.create_task(msg.add_reaction(reaction))

        def check(r, u):
            if not isinstance(r.emoji, str):
                return False
            if u != user:
                return False
            return r.emoji in reactions and r.message.id in [
                m["message"].id for m in messages
            ]

        while True:
            try:
                reaction, _ = await self.bot.wait_for("reaction_add",
                                                      check=check,
                                                      timeout=120)
            except asyncio.TimeoutError:
                break
            setting = next(m["setting"] for m in messages
                           if m["message"].id == reaction.message.id)
            settings[setting] = reactions[reaction.emoji]
            template = next(t for t in embed_templates
                            if t["setting"] == setting)
            embed = setting_embed(template)
            asyncio.create_task(reaction.message.edit(embed=embed))
            await self.bot.database.set(user,
                                        {"et_reminder_settings": settings},
                                        self)
        for message in to_cleanup:
            asyncio.create_task(message.delete())

    def generate_schedule(self):
        now = datetime.datetime.now(UTC_TZ)
        normal = self.gamedata["event_timers"]["bosses"]["normal"]
        hardcore = self.gamedata["event_timers"]["bosses"]["hardcore"]
        schedule = []
        counter = 0
        while counter < 12:
            for boss in normal:
                increment = datetime.timedelta(hours=boss["interval"] *
                                               counter)
                time = (datetime.datetime(
                    1, 1, 1, *boss["start_time"], tzinfo=UTC_TZ) + increment)
                if time.day != 1:
                    continue
                time = time.replace(year=now.year,
                                    month=now.month,
                                    day=now.day,
                                    tzinfo=UTC_TZ)
                output = {
                    "name": boss["name"],
                    "time": time,
                    "waypoint": boss["waypoint"],
                }
                schedule.append(output)
            counter += 1
        for boss in hardcore:
            for hours in boss["times"]:
                time = datetime.datetime.now(UTC_TZ)
                time = time.replace(hour=hours[0], minute=hours[1])
                output = {
                    "name": boss["name"],
                    "time": time,
                    "waypoint": boss["waypoint"],
                }
                schedule.append(output)
        return sorted(schedule, key=lambda t: t["time"].time())

    def get_upcoming_bosses(self, limit=8):
        upcoming_bosses = []
        time = datetime.datetime.now(UTC_TZ)
        counter = 0
        day = 0
        done = False
        while not done:
            for boss in self.boss_schedule:
                if counter == limit:
                    done = True
                    break
                boss_time = boss["time"]
                boss_time = boss_time + datetime.timedelta(days=day)
                if time < boss_time:
                    output = {
                        "name": boss["name"],
                        "time": f"<t:{int(boss_time.timestamp())}:t>",
                        "waypoint": boss["waypoint"],
                        "diff": boss_time - time,
                    }
                    upcoming_bosses.append(output)
                    counter += 1
            day += 1
        return upcoming_bosses

    def schedule_embed(self, limit=8):
        schedule = self.get_upcoming_bosses(limit)
        data = discord.Embed(title="Upcoming world bosses",
                             color=self.embed_color)
        for boss in schedule:
            value = "Time: {}\nWaypoint: {}".format(boss["time"],
                                                    boss["waypoint"])
            data.add_field(
                name="{} in {}".format(boss["name"],
                                       self.format_timedelta(boss["diff"])),
                value=value,
                inline=False,
            )
        data.set_footer(
            text="The timestamps are dynamically adjusted to your timezone",
            icon_url=self.bot.user.display_avatar.url,
        )
        return data

    def format_timedelta(self, td):
        hours, remainder = divmod(td.seconds, 3600)
        minutes, seconds = divmod(remainder, 60)
        if hours:
            return "{} hours and {} minutes".format(hours, minutes)
        else:
            return "{} minutes".format(minutes)

    async def timer_embed(self, ctx, group):
        time = datetime.datetime.now(datetime.timezone.utc)
        position = (
            60 * time.hour + time.minute
        ) % 120  # this gets the minutes elapsed in the current 2 hour window
        maps = self.gamedata["event_timers"][group]
        title = {
            "hot": "HoT Event Timer",
            "pof": "PoF Event Timer",
            "day": "Day/Night cycle",
            "eod": "End of Dragons",
            "ibs": "The Icebrood Saga"
        }.get(group)
        embed = discord.Embed(title=title,
                              color=await self.get_embed_color(ctx))
        for location in maps:
            duration_so_far = 0
            current_phase = None
            index = 0
            phases = location["phases"]
            # TODO null handling
            for i, phase in enumerate(phases):
                if position < duration_so_far:
                    break
                current_phase = phase["name"]
                index = i
                duration_so_far += phase["duration"]
            double = phases + phases
            for phase in double[index + 1:]:
                if not phase["name"]:
                    duration_so_far += phase["duration"]
                    continue
                if phase["name"] == current_phase:
                    duration_so_far += phase["duration"]
                    continue
                break
            next_phase = phase["name"]
            time_until = duration_so_far - position
            event_time = time + datetime.timedelta(minutes=time_until)
            timestamp = f"<t:{int(event_time.timestamp())}:R>"
            if current_phase:
                current = f"Current phase: **{current_phase}**"
            else:
                current = "No events currently active."
            value = (current +
                     "\nNext phase: **{}** {}".format(next_phase, timestamp))
            embed.add_field(name=location["name"], value=value, inline=False)
        embed.set_footer(text=self.bot.user.name,
                         icon_url=self.bot.user.display_avatar.url)
        return embed

    async def get_timezone(self, guild):
        if not guild:
            return UTC_TZ
        doc = await self.bot.database.get_guild(guild, self)
        if not doc:
            return UTC_TZ
        tz = doc.get("timezone")
        if tz:
            offset = datetime.timedelta(hours=tz)
            tz = datetime.timezone(offset)
        return tz or UTC_TZ

    def get_time_until_event(self, reminder):
        if reminder["type"] == "boss":
            time = datetime.datetime.now(UTC_TZ)
            day = 0
            done = False
            while not done:
                for boss in self.boss_schedule:
                    boss_time = boss["time"]
                    boss_time = boss_time + datetime.timedelta(days=day)
                    if time < boss_time:
                        if boss["name"] == reminder["name"]:
                            return int((
                                boss_time -
                                datetime.datetime.now(UTC_TZ)).total_seconds())
                day += 1
        time = datetime.datetime.utcnow()
        position = (60 * time.hour + time.minute) % 120
        for location in self.gamedata["event_timers"][reminder["group"]]:
            if location["name"] == reminder["map_name"]:
                duration_so_far = 0
                index = 0
                phases = location["phases"]
                for i, phase in enumerate(phases):
                    if position < duration_so_far:
                        break
                    index = i
                    duration_so_far += phase["duration"]
                index += 1
                if index == len(phases):
                    if phases[0]["name"] == phases[index - 1]["name"]:
                        index = 1
                        duration_so_far += phases[0]["duration"]
                    else:
                        index = 0
                for phase in phases[index:]:
                    if phase["name"] == reminder["name"]:
                        break
                    duration_so_far += phase["duration"]
                else:
                    for phase in phases:
                        if phase["name"] == reminder["name"]:
                            break
                        duration_so_far += phase["duration"]
                return (duration_so_far - position) * 60

    # TODO
    async def process_reminder(self, user, reminder, i):
        time = self.get_time_until_event(reminder)

        if time < reminder["time"] + 30:
            last_reminded = reminder.get("last_reminded")
            if (last_reminded and
                (datetime.datetime.utcnow() - last_reminded).total_seconds()
                    < reminder["time"] + 120):
                return
            try:
                last_message = reminder.get("last_message")
                if last_message:
                    last_message = await user.fetch_message(last_message)
                    await last_message.delete()
            except discord.HTTPException:
                pass
            when = datetime.datetime.now(
                datetime.timezone.utc) + datetime.timedelta(seconds=time)
            timestamp = f"<t:{int(when.timestamp())}:R>"
            description = f"{reminder['name']} will begin {timestamp}"
            embed = discord.Embed(title="Event reminder",
                                  description=description,
                                  color=self.embed_color)
            # try:
            try:

                msg = await user.send(
                    embed=embed, view=EventTimerReminderUnsubscribeView(self))
            except discord.HTTPException:
                return
            reminder["last_reminded"] = msg.created_at
            reminder["last_message"] = msg.id
            await self.bot.database.set(user,
                                        {f"event_reminders.{i}": reminder},
                                        self)

    @tasks.loop(seconds=10)
    async def event_reminder_task(self):
        cursor = self.bot.database.iter(
            "users", {"event_reminders": {
                "$exists": True,
                "$ne": []
            }}, self)
        async for doc in cursor:
            try:
                user = doc["_obj"]
                if not user:
                    continue
                for i, reminder in enumerate(doc["event_reminders"]):
                    asyncio.create_task(
                        self.process_reminder(user, reminder, i))
            except asyncio.CancelledError:
                return
            except Exception:
                pass

    @event_reminder_task.before_loop
    async def before_event_reminder_task(self):
        await self.bot.wait_until_ready()
Exemple #21
0
class ServerLogs(commands.GroupCog, name="serverlogs"):
    """Command group for accesing the server logs"""

    conn: asyncpg.Connection

    def __init__(self, bot):
        self.bot = bot
        self.emoji = discord.PartialEmoji.from_str('⚙')

    async def cog_load(self) -> None:
        try:
            self.conn = await asyncpg.connect(SERVER_LOGS_URL)
        except Exception as e:
            raise commands.ExtensionFailed(self.qualified_name, original=e)

    channel_blacklist = ['minecraft-console', 'dev-trusted']

    def build_query(
        self,
        message_content: Optional[str],
        member: Optional[int],
        channel: Optional[int],
        before: Optional[datetime],
        after: Optional[datetime],
        during: Optional[datetime],
        order: str,
        show_mod: bool,
        limit: int,
    ) -> tuple[str, list[Union[str, int, datetime]]]:
        sql_query = (
            "SELECT gm.created_at, gc.name, concat(u.name, '#',u.discriminator), gm.content FROM guild_messages gm "
            "INNER JOIN guild_channels gc ON gc.channel_id = gm.channel_id "
            "INNER JOIN users u ON u.user_id = gm.user_id  WHERE ")
        conditions = []
        bindings = []
        n_args = 1

        conditions.extend(f"gc.name NOT LIKE '%{c}%'"
                          for c in self.channel_blacklist)

        if not show_mod:
            conditions.append(
                "gc.name NOT LIKE 'mod%' and gc.name NOT LIKE 'server%'")

        if message_content:
            conditions.append(f"gm.content ~* ${n_args}")
            bindings.append(f"\\m{message_content}\\M")
            n_args = n_args + 1
        else:
            conditions.append("gm.content != ''")
        if member:
            conditions.append(f"u.user_id = ${n_args}")
            bindings.append(member)
            n_args = n_args + 1
        if channel:
            conditions.append(f"gc.channel_id = ${n_args}")
            bindings.append(channel)
            n_args = n_args + 1
        if before:
            conditions.append(f"gm.created_at < ${n_args}")
            bindings.append(before)
            n_args = n_args + 1
        if after:
            conditions.append(f"gm.created_at > ${n_args}")
            bindings.append(after)
            n_args = n_args + 1
        if during:
            conditions.append(f"gm.created_at::date = ${n_args}")
            bindings.append(during)
        sql_query += " AND ".join(conditions)
        sql_query += f" ORDER BY 2,1 {order} LIMIT {limit}"
        return sql_query, bindings

    @is_staff_app("OP")
    @app_commands.describe(
        message_content="What to search in the message contents.",
        member_id="ID or mention of User to search.",
        channel_id="ID or mention of channel to search in.",
        before="Date in yyyy-mm-dd format.",
        after="Date in yyyy-mm-dd format.",
        during="Date in yyyy-mm-dd format. Can't be used with before or after.",
        order_by=
        "Show old or new messages first. Defaults to newest messages first.",
        show_mod_channels=
        "If message in mod channels should be shown. Defaults to False",
        limit="Limit of message to fetch. Max 1000. Defaults to 500.",
        view_state=
        "If the results file should be public or in a ephemeral message. Defaults to public"
    )
    @app_commands.choices(order_by=[
        Choice(
            name='Older first',
            value='ASC',
        ),
        Choice(name='New first', value='DESC'),
    ],
                          view_state=[
                              Choice(name='Public', value=""),
                              Choice(name='Private', value="private")
                          ])
    @app_commands.command()
    async def search_messages(
        self,
        interaction: discord.Interaction,
        message_content: Optional[str] = None,
        member_id: app_commands.Transform[Optional[int],
                                          HackIDTransformer] = None,
        channel_id: app_commands.Transform[Optional[int],
                                           HackIDTransformer] = None,
        before: app_commands.Transform[Optional[datetime],
                                       DateTransformer] = None,
        after: app_commands.Transform[Optional[datetime],
                                      DateTransformer] = None,
        during: app_commands.Transform[Optional[datetime],
                                       DateTransformer] = None,
        order_by: str = "DESC",
        view_state: str = "",
        show_mod_channels: bool = False,
        limit: app_commands.Range[
            int, 1,
            1000] = 500  # limit might change in the future but 1000 is good for now
    ):
        """Search the server logs for messages that matches the parameters given then returns them in a file"""

        await interaction.response.defer(ephemeral=bool(view_state))

        if (after or before) and during:
            return await interaction.edit_original_message(
                content="You can't use after or before with during.")

        stmt, bindings = self.build_query(message_content, member_id,
                                          channel_id, before, after, during,
                                          order_by, show_mod_channels, limit)

        txt = ""

        async with self.conn.transaction():
            async for created_at, channel_name, username, content in self.conn.cursor(
                    stmt, *bindings):
                txt += f"[{channel_name}] [{created_at:%Y/%m/%d %H:%M:%S}] <{username} {content}>\n"

        if not txt:
            return await interaction.edit_original_message(
                content="No messages found.")

        txt_bytes = txt.encode("utf-8")

        if len(txt_bytes) > interaction.guild.filesize_limit:
            return await interaction.edit_original_message(
                content="Result is too big!")
        data = io.BytesIO(txt_bytes)
        file = discord.File(filename="output.txt", fp=data)
        await interaction.edit_original_message(attachments=[file])

    @is_staff_app("OP")
    @app_commands.describe(
        name="Name or part of the name of the channel to search",
        view_state=
        "If the results file should be public or in a ephemeral message. Defaults to public"
    )
    @app_commands.choices(
        view_state=[
            Choice(name='Public', value=""),
            Choice(name='Private', value="private")
        ], )
    @app_commands.command()
    async def search_channels(
        self,
        interaction: discord.Interaction,
        name: str = "",
        view_state: str = "",
    ):
        """Search the server logs for channels that matches the name given then returns them in a file"""

        await interaction.response.defer(ephemeral=bool(view_state))

        query = "SELECT channel_id, name, last_updated FROM guild_channels"
        args = []

        if name:
            query += " where guild_channels.name ~* $1"
            args.append(f".*{name}.*")

        txt = ""

        async with self.conn.transaction():
            async for channel_id, name, last_updated in self.conn.cursor(
                    query, *args):
                txt += f"{channel_id:18} | {name:50} | {last_updated:%Y/%m/%d %H:%M:%S}\n"

        if not txt:
            return await interaction.edit_original_message(
                content="No messages found.")

        # The padding won't be exact if the channel has unicode characters but oh well
        header = f"{'ID':18} | {'channel name':50} | {'last updated'}\n"
        txt_bytes = (header + txt).encode("utf-8")

        if len(txt_bytes) > interaction.guild.filesize_limit:
            return await interaction.edit_original_message(
                content="Result is too big!")
        data = io.BytesIO(txt_bytes)
        file = discord.File(filename="output.txt", fp=data)
        await interaction.edit_original_message(attachments=[file])
Exemple #22
0
        await self.bot.database.set(interaction.guild, settings, self)
        await interaction.response.send_message(
            f"I will now send update notifications to {channel.mention}.")

    @notifier_group.command(name="bosses")
    @app_commands.checks.has_permissions(manage_guild=True)
    @app_commands.guild_only()
    @app_commands.describe(
        enabled="Enable or disable boss notifier. "
        "If enabling, channel argument must be set",
        channel="The channel to post to.",
        behavior="Select behavior for posting/editing the message. Defaults to "
        "posting a new message")
    @app_commands.choices(behavior=[
        Choice(name="Delete the previous day's message. "
               "Causes an unread notification.",
               value="delete"),
        Choice(name="Edit the previous day's message. No unread "
               "notification, but bad for active channels",
               value="edit")
    ])
    async def bossnotifier(self,
                           interaction: discord.Interaction,
                           enabled: bool,
                           channel: discord.TextChannel = None,
                           behavior: str = "delete"):
        """Send the next two bosses every 15 minutes to a channel"""
        await interaction.response.defer(ephemeral=True)
        edit = behavior == "edit"
        if enabled and not channel:
            return await interaction.followup.send(
Exemple #23
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
class Usefull(commands.Cog, name="usefull"):
    """
		Usefull commands for Devs & more.

		Require intents:
			- message_content
		
		Require bot permission:
			- send_messages
	"""
    def __init__(self, bot: commands.Bot) -> None:
        self.bot = bot

    """def help_custom(self) -> tuple[str]:
		emoji = '🚩'
		label = "Usefull"
		description = "Usefull commands."
		return emoji, label, description"""

    @app_commands.command(name="reminder",
                          description="Reminds you of something.")
    @app_commands.describe(hours="Hours.",
                           minutes="Minutes.",
                           seconds="Seconds.",
                           message="Your reminder message.")
    @app_commands.choices(
        hours=[Choice(name=i, value=i) for i in range(0, 25)],
        minutes=[Choice(name=i, value=i) for i in range(0, 56, 5)],
        seconds=[Choice(name=i, value=i) for i in range(5, 56, 5)])
    @app_commands.checks.bot_has_permissions(send_messages=True)
    @app_commands.checks.has_permissions(use_slash_commands=True)
    async def reminder(self, interaction: discord.Interaction, hours: int,
                       minutes: int, seconds: int, message: str) -> None:
        """Reminds you of something."""
        remind_in = round(
            datetime.timestamp(
                datetime.now() +
                timedelta(hours=hours, minutes=minutes, seconds=seconds)))
        await interaction.response.send_message(
            f"Your message will be sent <t:{remind_in}:R>.")

        await asyncio.sleep(seconds + minutes * 60 + hours * (60**2))
        await interaction.channel.send(
            f":bell: <@{interaction.user.id}> Reminder (<t:{remind_in}:R>): {message}"
        )

    @app_commands.command(name="strawpoll", description="Create a strawpoll.")
    @app_commands.describe(question="The question of the strawpoll.")
    @app_commands.checks.has_permissions(use_slash_commands=True)
    async def avatar(self, interaction: discord.Interaction, question: str):
        await interaction.response.send_message(
            content=f"__*{interaction.user.mention}*__ : {question}",
            allowed_mentions=discord.AllowedMentions(everyone=False,
                                                     users=True,
                                                     roles=False))
        message = await interaction.original_message()
        await message.add_reaction("<a:checkmark_a:842800730049871892>")
        await message.add_reaction("<a:crossmark:842800737221607474>")

    @commands.command(name="emojilist", aliases=["ce", "el"])
    @commands.cooldown(1, 10, commands.BucketType.user)
    @commands.guild_only()
    async def getcustomemojis(self, ctx):
        """Return a list of each cutom emojis from the current server."""
        embed_list, embed = [], discord.Embed(
            title=f"Custom Emojis List ({len(ctx.guild.emojis)}) :")
        for i, emoji in enumerate(ctx.guild.emojis, start=1):
            if i == 0:
                i += 1
            value = f"`<:{emoji.name}:{emoji.id}>`" if not emoji.animated else f"`<a:{emoji.name}:{emoji.id}>`"
            embed.add_field(
                name=
                f"{self.bot.get_emoji(emoji.id)} - **:{emoji.name}:** - (*{i}*)",
                value=value)
            if len(embed.fields) == 25:
                embed_list.append(embed)
                embed = discord.Embed()
        if len(embed.fields) > 0:
            embed_list.append(embed)

        for message in embed_list:
            await ctx.send(embed=message)
Exemple #25
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 #26
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 #27
0
    )

    embed.add_field(
        name='Twitter',
        value='https://twitter.com/SpiderPigEthan',
        inline=False
    )

    await interaction.response.send_message(embed=embed)
    print(f'{client.user}({client.user.id}) executed Bot command.')

# SETROLE COMMAND #
valid_roles = ['sub', 'booster', 'mod', 'user', 'movie', 'game']
role_choices = []
for r in valid_roles:
    role_choices.append(Choice(name=r, value=r))

@client.tree.command(guild=discord.Object(id=server_id), name='setrole', description='Assign the specified role to a defined role variable in the bot.')
@app_commands.choices(role_name=role_choices)
async def setrole(interaction: discord.Interaction, role_name: str, role: discord.Role):
    if interaction.user.guild_permissions.administrator:
        print(f'{interaction.user}({interaction.user.id}) executed SetRole command.')
        role_name = role_name.lower()

        if await helpers.role_helper.is_role_defined(role_name):
            db.execute('UPDATE roles SET role_id=? WHERE role_name=?', (role.id, role_name))
        else:
            db.execute('INSERT INTO roles VALUES (?,?)', (role_name, role.id))
        print(f'{role_name} role set to {role}({role.id})')
        await interaction.response.send_message(
            embed=await helpers.embed_helper.create_success_embed(
Exemple #28
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)