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)
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)
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
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 ]
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]
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]
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]
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]
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
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()]
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]
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)
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)
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)
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}.")
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())
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.")
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")
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()
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])
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(
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)
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)
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)
) 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(
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)