From 173995c7543c6d7c7b3ee621be3bf914f5eeb18e Mon Sep 17 00:00:00 2001 From: MagicTheDev Date: Sun, 15 Dec 2024 21:59:29 -0600 Subject: [PATCH] some qol things --- background/tasks/background_cache.py | 8 + commands/clan/commands.py | 2 + commands/clan/utils.py | 4 +- commands/components/buttons.py | 10 +- commands/dev/commands.py | 5 +- commands/legends/commands.py | 2 + commands/player/commands.py | 4 + commands/ticketing/click.py | 43 +++-- discord/autocomplete.py | 6 + discord/events.py | 246 ++++++++++++++++++++------- utility/discord_utils.py | 12 ++ 11 files changed, 261 insertions(+), 81 deletions(-) diff --git a/background/tasks/background_cache.py b/background/tasks/background_cache.py index e838b32b..a4db5ac4 100644 --- a/background/tasks/background_cache.py +++ b/background/tasks/background_cache.py @@ -31,6 +31,14 @@ async def guilds_store(self): self.bot.SERVER_MAP = {g.id : g for shard in self.bot.SHARD_DATA for g in shard.servers} + channel = self.bot.get_channel(937528942661877851) + if channel is not None: + number_of_servers = len(list(self.bot.SERVER_MAP.keys())) + new_channel_name = f'ClashKing: {number_of_servers} Servers' + if new_channel_name != channel.name: + await channel.edit(name=new_channel_name) + + @guilds_store.before_loop async def before_guilds_store(self): await self.bot.wait_until_ready() diff --git a/commands/clan/commands.py b/commands/clan/commands.py index 4724db55..fc9209c7 100644 --- a/commands/clan/commands.py +++ b/commands/clan/commands.py @@ -1,6 +1,7 @@ from disnake.ext import commands from discord import autocomplete, options +from utility.discord_utils import user_command from .utils import * @@ -9,6 +10,7 @@ class ClanCommands(commands.Cog, name='Clan Commands'): def __init__(self, bot: CustomClient): self.bot = bot + @user_command() @commands.slash_command(name='clan') async def clan(self, ctx: disnake.ApplicationCommandInteraction): await ctx.response.defer() diff --git a/commands/clan/utils.py b/commands/clan/utils.py index ee5dfb90..440e3ec3 100644 --- a/commands/clan/utils.py +++ b/commands/clan/utils.py @@ -23,7 +23,9 @@ @register_button('clandetailed', parser='_:clan') async def detailed_clan_board(bot: CustomClient, clan: coc.Clan, server: disnake.Guild, embed_color: disnake.Color): - db_clan = await bot.clan_db.find_one({'$and': [{'tag': clan.tag}, {'server': server.id}]}) + db_clan = None + if server: + db_clan = await bot.clan_db.find_one({'$and': [{'tag': clan.tag}, {'server': server.id}]}) clan_legend_ranking = await bot.clan_leaderboard_db.find_one({'tag': clan.tag}) diff --git a/commands/components/buttons.py b/commands/components/buttons.py index 19c18aaa..85785522 100644 --- a/commands/components/buttons.py +++ b/commands/components/buttons.py @@ -13,7 +13,7 @@ async def button_logic( bot: CustomClient, guild: disnake.Guild, locale: disnake.Locale, - ctx: disnake.MessageInteraction = None, + ctx: disnake.MessageInteraction | None = None, autoboard: bool = False, ): split_data = button_data.split(':') @@ -57,10 +57,14 @@ async def button_logic( elif name == 'custom_player': data = await bot.getPlayer(player_tag=data, custom=True) elif name == 'discord_user': - data = await ctx.guild.getch_member(data) + ctx: disnake.ApplicationCommandInteraction + if ctx.guild: + data = await ctx.guild.getch_member(data) + else: + data = ctx.user hold_kwargs[name] = data - embed_color = await bot.ck_client.get_server_embed_color(server_id=guild.id) + embed_color = await bot.ck_client.get_server_embed_color(server_id=None if not guild else guild.id) hold_kwargs['embed_color'] = embed_color hold_kwargs = {key: hold_kwargs[key] for key in inspect.getfullargspec(function).args} embed = await function(**hold_kwargs) diff --git a/commands/dev/commands.py b/commands/dev/commands.py index 7115098b..8228e07a 100644 --- a/commands/dev/commands.py +++ b/commands/dev/commands.py @@ -15,6 +15,7 @@ import random from classes.bot import CustomClient from discord import convert, autocomplete +from utility.constants import EMBED_COLOR_CLASS class OwnerCommands(commands.Cog): def __init__(self, bot: CustomClient): @@ -135,8 +136,8 @@ async def test(self, ctx: ApplicationCommandInteraction): pass - @dev.sub_command(name='autoboard-limit', description="Set a new autoboard limit for a server") - async def autoboard_limit(self, ctx: ApplicationCommandInteraction, + @dev.sub_command(name='automation-limit', description="Set a new autoboard limit for a server") + async def automation_limit(self, ctx: ApplicationCommandInteraction, server: disnake.Guild =commands.Param(converter=convert.server, autocomplete=autocomplete.all_server), new_limit: int = commands.Param()): await ctx.response.defer() diff --git a/commands/legends/commands.py b/commands/legends/commands.py index 1654986f..f333e76d 100644 --- a/commands/legends/commands.py +++ b/commands/legends/commands.py @@ -6,6 +6,7 @@ from discord import autocomplete, convert, options from exceptions.CustomExceptions import PlayerNotInLegends from utility.constants import POSTER_LIST +from utility.discord_utils import user_command from .utils import ( legend_buckets, @@ -23,6 +24,7 @@ class Legends(commands.Cog): def __init__(self, bot: CustomClient): self.bot = bot + @user_command() @commands.slash_command( name=disnake.Localized('legends', key='legends-name'), description=disnake.Localized(key='legends-description'), diff --git a/commands/player/commands.py b/commands/player/commands.py index cb4c8cef..b5add671 100644 --- a/commands/player/commands.py +++ b/commands/player/commands.py @@ -1,5 +1,6 @@ import disnake from disnake.ext import commands +from disnake.ext.commands import dm_only from classes.bot import CustomClient from classes.player.stats import StatsPlayer @@ -7,6 +8,7 @@ from exceptions.CustomExceptions import * from utility.player_pagination import button_pagination from utility.search import search_results +from utility.discord_utils import user_command from .utils import player_accounts, to_do_embed @@ -17,6 +19,7 @@ class PlayerCommands(commands.Cog, name='Player Commands'): def __init__(self, bot: CustomClient): self.bot = bot + @user_command() @commands.slash_command( name=disnake.Localized('player', key='player-name'), description=disnake.Localized(key='player-description'), @@ -24,6 +27,7 @@ def __init__(self, bot: CustomClient): async def player(self, ctx: disnake.ApplicationCommandInteraction): await ctx.response.defer() + @player.sub_command( name=disnake.Localized('lookup', key='lookup-name'), description='Lookup a player or discord user', diff --git a/commands/ticketing/click.py b/commands/ticketing/click.py index d39b4ab5..cc027d2f 100644 --- a/commands/ticketing/click.py +++ b/commands/ticketing/click.py @@ -1,12 +1,11 @@ from datetime import datetime -from datetime import datetime from typing import List import coc import disnake import pytz from disnake.ext import commands - +import pendulum as pend from classes.bot import CustomClient from classes.player.stats import StatsPlayer from classes.tickets import LOG_TYPE, OpenTicket, TicketPanel @@ -201,14 +200,36 @@ def check(res): ) await ctx.channel.delete() - panel_settings = await self.bot.tickets.find_one( - { - '$and': [ - {'server_id': ctx.guild.id}, - {'name': ctx.data.custom_id.split('_')[0]}, - ] - } - ) + def is_unix_timestamp(timestamp: str) -> bool: + """ + Check if a string is a valid Unix timestamp. + + Args: + timestamp (str): The string to check. + + Returns: + bool: True if the string is a valid Unix timestamp, False otherwise. + """ + try: + # Ensure the string can be converted to an integer + ts_int = int(timestamp) + + # Check if the timestamp is within a reasonable range for Unix time + pend.from_timestamp(ts_int) + return True + except (ValueError, OverflowError): + return False + + panel_settings = None + if is_unix_timestamp(ctx.data.custom_id.split("_")[-1]): + panel_settings = await self.bot.tickets.find_one( + { + '$and': [ + {'server_id': ctx.guild.id}, + {'name': ctx.data.custom_id.split('_')[0]}, + ] + } + ) if panel_settings is not None: member = await ctx.guild.getch_member(ctx.user.id) panel = TicketPanel(bot=self.bot, panel_settings=panel_settings) @@ -320,14 +341,12 @@ def check(res): else: await res.response.send_message(content='Done!', components=[], ephemeral=True) - print(button.questions) if button.questions: if message: await message.delete() (message, questionaire_embed) = await ask_questions(bot=self.bot, ctx=ctx, questions=button.questions) embeds.append(questionaire_embed) - print("here again") channels = await open_ticket( bot=self.bot, ticket_panel=panel, diff --git a/discord/autocomplete.py b/discord/autocomplete.py index ff42841e..6cf463e6 100644 --- a/discord/autocomplete.py +++ b/discord/autocomplete.py @@ -39,7 +39,13 @@ async def category(self, ctx: disnake.ApplicationCommandInteraction, query: str) async def clan(self, ctx: disnake.ApplicationCommandInteraction, query: str): if ctx.guild is None: + last_record = await self.bot.command_stats.find_one( + {"$and" : [{"user" : ctx.user.id}, {"server": {"$ne": None}}]}, + sort=[("time", -1)] + ) guild_id = 0 + if last_record: + guild_id = last_record.get("server") else: guild_id = ctx.guild.id if ctx.filled_options.get('family') is not None: diff --git a/discord/events.py b/discord/events.py index 5b4c6651..74d8255b 100644 --- a/discord/events.py +++ b/discord/events.py @@ -9,8 +9,8 @@ from classes.bot import CustomClient from classes.DatabaseClient.familyclient import FamilyClient from classes.tickets import LOG_TYPE, OpenTicket, TicketPanel -from utility.constants import DISCORD_STATUS_TYPES -from utility.discord_utils import get_webhook_for_channel +from utility.constants import DISCORD_STATUS_TYPES, EMBED_COLOR_CLASS +from utility.discord_utils import get_webhook_for_channel, PATCHABLE_COMMANDS from utility.startup import fetch_emoji_dict @@ -29,12 +29,13 @@ async def on_connect(self): self.bot.emoji = Emojis(bot=self.bot) self.bot.ck_client = FamilyClient(bot=self.bot) - if self.bot.user.id == 808566437199216691: + logger.info('We have connected to the discord gateway') + + if not self.bot.user.public_flags.verified_bot: return global has_started if not has_started: - await asyncio.sleep(60) has_started = True database_guilds = await self.bot.server_db.distinct('server') database_guilds: set = set(database_guilds) @@ -56,23 +57,15 @@ async def on_connect(self): except: continue - logger.info('We have connected') @commands.Cog.listener() async def on_ready(self): global has_started - if not has_started: - await asyncio.sleep(15) - has_started = True - else: + if has_started: return - """ for command in self.bot.global_application_commands: - print(f"Patching command integration type for {command.id}") - await self.bot.http.edit_global_command( - self.bot.application_id, - command.id, - payload={"integration_types": [0, 1], "contexts": [0, 1, 2]}, - )""" + + if self.bot._config.cluster_id == 0: + await self.sync_patchable_commands() if self.bot.user.public_flags.verified_bot: for count, shard in self.bot.shards.items(): @@ -85,29 +78,81 @@ async def on_ready(self): 'activity_text': 'Use Code ClashKing 👀', 'status': 'Online', } - bot_settings = await self.bot.custom_bots.find_one({'token': self.bot._config.bot_token}) - if bot_settings: - default_status = bot_settings.get('state', default_status) await self.bot.change_presence( activity=disnake.CustomActivity(state=default_status.get('activity_text'), name='Custom Status'), status=DISCORD_STATUS_TYPES.get(default_status.get('status')), ) - logger.info('ready') + logger.info('Bot has loaded & is ready') + + @commands.Cog.listener() async def on_guild_join(self, guild: disnake.Guild): if not self.bot.user.public_flags.verified_bot: return + + log_channel = await self.bot.getch_channel(937519135607373874) + + server_name = guild.name + server_id = guild.id + owner = guild.owner + member_count = guild.member_count + roles = len(guild.roles) + channels = len([ch for ch in guild.channels if str(ch.type) in ['text', 'voice']]) + text_channels = len([ch for ch in guild.channels if str(ch.type) == 'text']) + voice_channels = len([ch for ch in guild.channels if str(ch.type) == 'voice']) + creation_date = pend.instance(guild.created_at).to_datetime_string() # Format: YYYY-MM-DD HH:mm:ss + description = guild.description or "No description provided" + icon_url = guild.icon.url if guild.icon else None + banner_url = guild.banner.url if guild.banner else None + bot_admin = (await guild.getch_member(self.bot.user.id)).guild_permissions.administrator + + # Check boost status + boost_level = guild.premium_tier + boosts = guild.premium_subscription_count + + # Construct embed with all details + embed = disnake.Embed( + title="Joined a New Server! 🎉", + description=f"**ClashKing has joined the server:** `{server_name}`", + color=disnake.Color.green(), + timestamp=pend.now() + ) + + # Add fields with detailed server info + embed.add_field(name="Server ID", value=f"{server_id}", inline=True) + embed.add_field(name="Owner", value=f"{owner}", inline=True) + embed.add_field(name="Member Count", value=f"{member_count}", inline=True) + embed.add_field(name="Total Roles", value=f"{roles}", inline=True) + embed.add_field(name="Total Channels", value=f"{channels} (Text: {text_channels}, Voice: {voice_channels})", + inline=False) + embed.add_field(name="Creation Date", value=f"{creation_date}", inline=True) + embed.add_field(name="Server Description", value=f"{description}", inline=False) + embed.add_field(name="Boost Level", value=f"Tier {boost_level} with {boosts} Boosts", inline=True) + embed.add_field(name="Admin Permissions?", value="✅ Yes" if bot_admin else "❌ No", inline=True) + + if icon_url: + embed.set_thumbnail(url=icon_url) + if banner_url: + embed.set_image(url=banner_url) + + await log_channel.send(embed=embed) + msg = ( - 'Thanks for inviting ClashKing to your server! It comes packed with a decent amount of features like legends tracking, autoboards, a plethora of stats, & the ability to help manage your clan & families with features like role management, ticketing, & rosters. I recommend starting by taking a look over the entire `/help` command, ' - "in general I have done my best to make the names self explanatory (aside from a few legacy names, like /check being legend commands, don't ask why lol). " - "If you need any further help, don't hesitate to check out the documentation (in progress) or join my support server (I don't mind being a walking encyclopedia of answers to your questions).\n" - 'One last thing to note - the bot is still under heavy development & is relatively young, so please reach out with any issues & if you end up enjoying what is here, consider using Creator Code ClashKing ' - 'in-game to help support the project' + "# Thanks for inviting **ClashKing**! 🎉\n\n" + "ClashKing is designed to simplify clan and family management while providing powerful features like **legends tracking**, **autoboards**, and **in-depth stats**. " + "It also includes tools like **role management**, **ticketing**, and **roster management** to make running your clan easier.\n\n" + "To get started, browse the [documentation](https://docs.clashking.xyz) & run `/help` to explore the available features." + "For additional support, you can also query our docs with `/ask`) or join the [support/community server](https://discord.gg/clashking).\n\n" + "**Note:** ClashKing is actively developed and improving constantly. If you encounter any issues or have feature suggestions, let me know! If you enjoy the bot, " + "consider supporting the project by using Creator Code **ClashKing** in-game. Thank you for being part of this journey! - Destinea, Magic, & Obno ❤️" ) + + # Check if server settings exist in the database results = await self.bot.server_db.find_one({'server': guild.id}) - botAdmin = (await guild.getch_member(self.bot.user.id)).guild_permissions.administrator + + # Insert default server settings if none exist if results is None: await self.bot.server_db.insert_one( { @@ -121,52 +166,84 @@ async def on_guild_join(self, guild: disnake.Guild): 'lbhour': None, } ) - # if there's a result and bot has admin perms then no msg needed. - if results and botAdmin is True: + + if results and bot_admin: return - channel = self.bot.get_channel(937519135607373874) - await channel.send(f'Just joined {guild.name}') - len_g = len(self.bot.guilds) - for count, shard in self.bot.shards.items(): - await self.bot.change_presence( - activity=disnake.CustomActivity(state='Use Code ClashKing 👀', name='Custom Status'), - shard_id=shard.id, - ) # type 3 watching type#1 - playing - - channel = self.bot.get_channel(937528942661877851) - await channel.edit(name=f'ClashKing: {len_g} Servers') - - # loop channels to find the first text channel with perms to send message. - for guildChannel in guild.channels: - permissions = guildChannel.permissions_for(guildChannel.guild.me) - if str(guildChannel.type) == 'text' and permissions.send_messages is True: - firstChannel = guildChannel - break - else: + first_channel = next( + (channel for channel in guild.channels + if str(channel.type) == 'text' and channel.permissions_for(channel.guild.me).send_messages), + None + ) + if not first_channel: return - embed = disnake.Embed(description=msg, color=disnake.Color.blue()) + + embed = disnake.Embed(description=msg, color=EMBED_COLOR_CLASS) embed.set_thumbnail(url=self.bot.user.display_avatar.url) + buttons = disnake.ui.ActionRow() buttons.append_item(disnake.ui.Button(label='Support Server', emoji='🔗', url='https://discord.gg/clashking')) buttons.append_item(disnake.ui.Button(label='Documentation', emoji='🔗', url='https://docs.clashking.xyz')) - (embed.set_footer(text='Admin permissions are recommended for full functionality & easier set up, thank you!') if not botAdmin else None) - (await firstChannel.send(components=buttons, embed=embed) if results is None else None) + + # Add a footer if the bot lacks admin permissions + if not bot_admin: + embed.set_footer( + text='Admin permissions are recommended for full functionality and easier setup. Thank you!') + + # Send the message only if the server settings were just created + if results is None: + await first_channel.send(embed=embed, components=buttons) + + @commands.Cog.listener() async def on_guild_remove(self, guild): if not self.bot.user.public_flags.verified_bot: return - channel = self.bot.get_channel(937519135607373874) - await channel.send(f'Just left {guild.name}, {guild.member_count} members') - len_g = len(self.bot.guilds) - for count, shard in self.bot.shards.items(): - await self.bot.change_presence( - activity=disnake.CustomActivity(state='Use Code ClashKing 👀', name='Custom Status'), - shard_id=shard.id, - ) # type 3 watching type#1 - playing - channel = self.bot.get_channel(937528942661877851) - await channel.edit(name=f'ClashKing: {len_g} Servers') + log_channel = await self.bot.getch_channel(937519135607373874) + + server_name = guild.name + server_id = guild.id + owner = guild.owner + member_count = guild.member_count + roles = len(guild.roles) + channels = len([ch for ch in guild.channels if str(ch.type) in ['text', 'voice']]) + text_channels = len([ch for ch in guild.channels if str(ch.type) == 'text']) + voice_channels = len([ch for ch in guild.channels if str(ch.type) == 'voice']) + creation_date = pend.instance(guild.created_at).to_datetime_string() # Format: YYYY-MM-DD HH:mm:ss + description = guild.description or "No description provided" + icon_url = guild.icon.url if guild.icon else None + banner_url = guild.banner.url if guild.banner else None + bot_admin = (await guild.getch_member(self.bot.user.id)).guild_permissions.administrator + + boost_level = guild.premium_tier + boosts = guild.premium_subscription_count + + embed = disnake.Embed( + title="Left a Server 🛑", + description=f"**ClashKing has been removed from the server:** `{server_name}`", + color=disnake.Color.red(), + timestamp=pend.now() + ) + + embed.add_field(name="Server ID", value=f"{server_id}", inline=True) + embed.add_field(name="Owner", value=f"{owner}", inline=True) + embed.add_field(name="Member Count", value=f"{member_count}", inline=True) + embed.add_field(name="Total Roles", value=f"{roles}", inline=True) + embed.add_field(name="Total Channels", value=f"{channels} (Text: {text_channels}, Voice: {voice_channels})", + inline=False) + embed.add_field(name="Creation Date", value=f"{creation_date}", inline=True) + embed.add_field(name="Server Description", value=f"{description}", inline=False) + embed.add_field(name="Boost Level", value=f"Tier {boost_level} with {boosts} Boosts", inline=True) + embed.add_field(name="Admin Permissions?", value="✅ Yes" if bot_admin else "❌ No", inline=True) + + if icon_url: + embed.set_thumbnail(url=icon_url) + if banner_url: + embed.set_image(url=banner_url) + + await log_channel.send(embed=embed) + @commands.Cog.listener() async def on_application_command(self, ctx: disnake.ApplicationCommandInteraction): @@ -236,7 +313,7 @@ async def on_application_command(self, ctx: disnake.ApplicationCommandInteractio ) sent_support_msg = True except Exception as e: - print(f"Error in sending weekly support message: {e}") + pass await self.bot.command_stats.insert_one( { @@ -247,16 +324,17 @@ async def on_application_command(self, ctx: disnake.ApplicationCommandInteractio 'time': int(datetime.datetime.now().timestamp()), 'guild_size': ctx.guild.member_count if ctx.guild is not None else 0, 'channel': ctx.channel_id, - 'channel_name': ctx.channel.name if ctx.channel is not None else None, + 'channel_name': ctx.channel.name if ctx.channel is not None and hasattr(ctx.channel, "name") else None, 'len_mutual': len(ctx.user.mutual_guilds), 'is_bot_dev': ctx.user.public_flags.verified_bot_developer, 'bot': ctx.bot.user.id, 'sent_support_msg': sent_support_msg, 'interaction_id' : ctx.id, - 'options' : ctx.filled_options + 'options' : str(ctx.filled_options) } ) + @commands.Cog.listener() async def on_member_join(self, member: disnake.Member): if member.guild.id not in self.bot.OUR_GUILDS: @@ -367,5 +445,47 @@ async def on_raw_member_remove(self, payload: disnake.RawGuildMemberRemoveEvent) await channel.delete(reason=f'{payload.user.name} left server') + async def sync_patchable_commands(self): + """ + Sync global commands to patch or revert top-level commands as needed. + """ + # Fetch all current global commands from Discord + global_commands = await self.bot.http.get_global_commands(self.bot.application_id) + + # Desired patchable contexts + PATCH_CONTEXTS = {"integration_types": [0, 1], "contexts": [0, 1, 2]} # Patched + DEFAULT_CONTEXTS = {"integration_types": [0], "contexts": None} # Normal slash commands + + # Iterate through current global commands + for command in global_commands: + command_name = command["name"] + command_id = command["id"] + + # Check if the command is in the PATCHABLE_COMMANDS registry + if command_name in PATCHABLE_COMMANDS: + await self._update_command_if_needed(command_id, PATCH_CONTEXTS, command) + else: + await self._update_command_if_needed(command_id, DEFAULT_CONTEXTS, command) + + + async def _update_command_if_needed(self, command_id, desired_contexts, command): + """ + Updates a command's contexts only if they differ from the desired state. + """ + current_contexts = { + "integration_types": command.get("integration_types", []), + "contexts": command.get("contexts", []) + } + # Only send API call if there's a difference + if current_contexts != desired_contexts: + await self.bot.http.edit_global_command( + self.bot.application_id, + command_id, + payload=desired_contexts, + ) + else: + pass + + def setup(bot: CustomClient): bot.add_cog(DiscordEvents(bot)) diff --git a/utility/discord_utils.py b/utility/discord_utils.py index e56cbb11..e1c7c967 100644 --- a/utility/discord_utils.py +++ b/utility/discord_utils.py @@ -237,6 +237,18 @@ def decorator(func: Callable[..., None]) -> Callable[..., None]: return decorator +PATCHABLE_COMMANDS = set() +def user_command(): + """ + Decorator to mark a command as patchable for global availability. + Adds the command's name to the PATCHABLE_COMMANDS registry. + """ + def wrapper(command_obj): + # Add the command's name to the global registry + PATCHABLE_COMMANDS.add(command_obj.name) + return command_obj + return wrapper + async def get_webhook_for_channel(bot, channel: Union[disnake.TextChannel, disnake.Thread]) -> disnake.Webhook: try: