async def setupCommands(self): defaultOptions=[discord.ApplicationCommandOption( name="input", description="Command input data.", type=discord.ApplicationCommandOptionType.string, required=False )] commandsToAdd = [ {"name": "raidtime", "description": "Moves the raiders to the raid channel.", "options": []}, {"name": "endraid", "description": "Moves the raiders to the raid channel.", "options": defaultOptions}, {"name":"play", "description":"Plays TTS audio in voice channel.", "options":defaultOptions}, {"name":"playnext", "description":"Adds audio to the next slot in the queue.", "options":defaultOptions}, {"name":"stop", "description":"Stops audio being played in the voice channel.", "options":[]}, {"name":"pause", "description":"Pauses audio being playing in the voice channel.", "options":[]}, {"name":"resume", "description":"Resumes audio being playing in the voice channel.", "options":[]}, #{"name":"skip", "description":"Skips audio being played to the next audio in the queue in the voice channel.", "options":[]}, {"name":"clear", "description":"Clears the queue.", "options":[]}, {"name":"volume", "description":"Changes the volume.", "options":defaultOptions}, #{"name":"loop", "description":"Enables/Disables the looping of the queue.", "options":defaultOptions}, #{"name":"repeat", "description":"Enables/Disables the repeat of an audio.", "options":defaultOptions}, #{"name":"shuffle", "description":"Shuffles the queue.", "options":defaultOptions}, {"name":"join", "description":"Joins a voice channel.", "options":defaultOptions}, {"name":"leave", "description":"Leaves the voice channel.", "options":[]}, ] for command_ in commandsToAdd: newCommand = discord.ApplicationCommand( name=command_["name"], description=command_["description"], options=command_["options"], ) self.commands.append(newCommand)
def __init__(self): super().__init__() self.loop = asyncio.get_event_loop() self.discordCredential: credentials.Discord_Credential self.cooldownModule:Cooldown_Module = Cooldown_Module() self.cooldownModule.setupCooldown("discordRateLimit", 10, 1) # don't freak out, this is *merely* a regex for matching urls that will hit just about everything self._urlMatcher = re.compile( "(https?:(/{1,3}|[a-z0-9%])|[a-z0-9.-]+[.](com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))") self.guild = discord.Object(id=197565046916775945) self.commands = [ discord.ApplicationCommand( name="math", description="Solves your math problem.", options=[discord.ApplicationCommandOption( name="equation", description="The equation you want to solve.", type=discord.ApplicationCommandOptionType.string, )], ), discord.ApplicationCommand( name="test", description="Does a variety of tests.", options=[discord.ApplicationCommandOption( name="input", description="The input data.", type=discord.ApplicationCommandOptionType.string, required=False )], ), ] self.VC_Channel:discord.channel.VoiceChannel = None self.voiceClient = None self.voice_isQueueEnabled = True self.voice_isQueueLooping = False self.voice_isQueueRepeating = False self.voice_isQueueShuffle = False self.voiceQueue = [] self.DB = bot_functions.utilities_db.Praxis_DB_Connection() self.tasks = []
class WolframAlpha(vbu.Cog): @commands.command( aliases=["wf", "wolframalpha", "wfa"], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="search", description="Your query for WolframAlpha.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set("api_keys", "wolfram") async def wolfram(self, ctx: vbu.Context, *, search: str): """ Send a query to WolframAlpha. """ # Build our request params = { "input": search, "appid": self.bot.config['api_keys']['wolfram'], "format": "image", "output": "json", } headers = { "User-Agent": self.bot.user_agent, } # Send our request async with self.bot.session.get( "https://api.wolframalpha.com/v2/query", params=params, headers=headers) as r: data = json.loads(await r.text()) # Send output try: pod = data['queryresult']['pods'][1] embed = vbu.Embed( title=pod['title'], use_random_colour=True, ).set_image(url=pod['subpods'][0]['img']['src'], ) return await ctx.send(embed=embed) except (KeyError, IndexError): return await ctx.send("No results for that query!")
"""SELECT * FROM github_repo_uses WHERE user_id=$1 ORDER BY uses DESC""", interaction.user.id, ) responses = [ discord.ApplicationCommandOptionChoice(name=(repo := str(GitRepo(r['host'], r['owner'], r['repo']))), value=repo) for r in rows ] return await interaction.response.send_autocomplete(responses) @issue.command( name="list", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="repo", description="The repo that you want to list the issues on.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="list_closed", description="Whether or not you want to list closed issues.", type=discord.ApplicationCommandOptionType.boolean, required=False, ), ], ), ) async def issue_list(self, ctx: vbu.Context, repo: GitRepo, list_closed: bool = False): """ List all of the issues on a git repo. """
width = image.width height = image.height initial_size = len(image.tobytes()) magic_constant = (initial_size) / (width * height) # The constant (c) size_mod = intended_size / ( magic_constant * width * height ) # The size modifier to reach the intended size return (int(width * size_mod), int(height * size_mod)) @commands.command( aliases=['stealemoji'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="emoji", description="The emoji that you want to add.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="name", description="The name of the emoji.", type=discord.ApplicationCommandOptionType.string, required=False, ), discord.ApplicationCommandOption( name="animated", description="Whether or not the emoji is animated.", type=discord.ApplicationCommandOptionType.boolean, required=False), ], ), )
return await ctx.send("I couldn't regex that message for codeblocks.") return await ctx.send(f"```{block_match.group(1)}\n{textwrap.dedent(block_match.group(2))}\n```") @commands.group(application_command_meta=commands.ApplicationCommandMeta()) async def rtfm(self, ctx): """ Get some data from the docs. """ @rtfm.command( name="djs", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="obj", description="The object that you want to look up.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def rtfm_djs(self, ctx: vbu.Context, *, obj: str): """ Get an item from the Discord.js documentation. """ await self.do_rtfm(ctx, "djs", obj) @rtfm.command( name="jda",
class MovieCommand(vbu.Cog): async def send_omdb_query(self, name: str, media_type: str, search: bool, year=None) -> dict: """ Send a query to the OMDB API, returning the results dict. """ # Build up the params key = 's' if search else 't' params = { 'apikey': self.bot.config['api_keys']['omdb'], 'type': media_type, key: name, } if year: params.update({'year': year}) headers = {"User-Agent": self.bot.user_agent} # Send the request async with self.bot.session.get("http://www.omdbapi.com/", params=params, headers=headers) as r: data = await r.json() return data def generate_embed(self, data) -> typing.Optional[vbu.Embed]: """ Make an embed based on some OMDB data. """ search = data.get('Title') is None if search and data.get('Search') is None: return None embed = vbu.Embed(use_random_colour=True) if not search: embed.title = f"{data['Title']} ({data['Year']})" valid_info = lambda v: v not in [None, 'N/A', 'n/a'] # List short details of up to 10 results if search: description_list = [] for index, row in enumerate(data['Search'][:10], start=1): if valid_info(row.get('Poster')): description_list.append( f"{index}. **{row['Title']}** ({row['Year']}) - [Poster]({row['Poster']})" ) else: description_list.append( f"{index}. **{row['Title']}** ({row['Year']})") embed.description = '\n'.join(description_list) return embed # List full details if data.get('Plot'): embed.description = data['Plot'] if data.get('Released'): embed.add_field("Release Date", data['Released']) if data.get('Rated'): embed.add_field("Age Rating", data['Rated']) if data.get('Runtime'): embed.add_field("Runtime", data['Runtime']) if data.get('Genre'): embed.add_field(f"Genre{'s' if ',' in data['Genre'] else ''}", data['Genre']) if data.get('imdbRating'): embed.add_field("IMDB Rating", data['imdbRating']) if data.get('Production'): embed.add_field( f"Production Compan{'ies' if ',' in data['Production'] else 'y'}", data['Production']) if data.get('Director'): embed.add_field( f"Director{'s' if ',' in data['Director'] else ''}", data['Director']) if data.get('Writer'): embed.add_field(f"Writer{'s' if ',' in data['Writer'] else ''}", data['Writer'], inline=False) if data.get('imdbID'): embed.add_field( "IMDB Page", f"[Direct Link](https://www.imdb.com/title/{data['imdbID']}/) - IMDB ID `{data['imdbID']}`", inline=False) if valid_info(data.get('Poster')): embed.set_thumbnail(data['Poster']) return embed @commands.group( invoke_without_command=True, application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'omdb') async def movie(self, ctx: vbu.Context): """ The parent command for movie commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @movie.group( name="get", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="name", description="The name of the movie that you want to get.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="year", description="The year that the movie was released.", type=discord.ApplicationCommandOptionType.integer, min_value=1800, max_value=2100, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'omdb') async def movie_get(self, ctx: vbu.Context, *, name: str, year: int = None): """ Gets a movie from the OMDB API. """ # Try and return the found data data = await self.send_omdb_query(name, 'movie', False, year) embed = self.generate_embed(data) if not embed: return await ctx.send( f"Couldn't find any results for **{name}**", allowed_mentions=discord.AllowedMentions.none(), ) return await ctx.send(embed=embed) # @movie.group(name="search") # @commands.bot_has_permissions(send_messages=True, embed_links=True) # @vbu.checks.is_config_set('api_keys', 'omdb') # async def movie_search(self, ctx: vbu.Context, *, name: str): # """ # Searches for a movie on the OMDB API. # """ # # See if we gave a year # original_name = name # if name.split(' ')[-1].isdigit() and int(name.split(' ')[-1]) > 1900: # *name, year = name.split(' ') # name = ' '.join(name) # else: # year = None # # Try and return the found data # await ctx.defer() # data = await self.send_omdb_query(name,'movie',True,year) # embed = self.generate_embed(data) # if not embed: # return await ctx.send(f"No movie results for `{original_name}` could be found.", allowed_mentions=discord.AllowedMentions.none()) # return await ctx.send(embed=embed) @commands.group( invoke_without_command=True, application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'omdb') async def tv(self, ctx: vbu.Context): """ The parent group for the TV commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @tv.command( name="get", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="name", description="The name of the TV show that you want to get.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="year", description="The year that the TV show was released.", type=discord.ApplicationCommandOptionType.integer, min_value=1800, max_value=2100, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'omdb') async def tv_get(self, ctx: vbu.Context, *, name: str, year: int = None): """ Gets a TV show from the OMDB API. """ # Try and return the found data data = await self.send_omdb_query(name, 'series', False, year) embed = self.generate_embed(data) if not embed: return await ctx.send( f"Couldn't find any results for **{name}**", allowed_mentions=discord.AllowedMentions.none(), ) return await ctx.send(embed=embed)
class UserInfo(vbu.Cog): @commands.command( aliases=["avatar", "av"], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="target", description="The item that you want to enlarge.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) async def enlarge( self, ctx: vbu.Context, target: typing.Union[discord.Member, discord.User, discord.Emoji, discord.PartialEmoji] = None, ): """ Enlarges the avatar or given emoji. """ target = target or ctx.author if isinstance(target, (discord.User, discord.Member, discord.ClientUser)): url = target.display_avatar.url elif isinstance(target, (discord.Emoji, discord.PartialEmoji)): url = target.url with vbu.Embed(color=0x1) as embed: embed.set_image(url=str(url)) await ctx.send(embed=embed) @commands.context_command(name="Get user info") async def _get_user_info(self, ctx: vbu.SlashContext, user: discord.Member): command = self.whois await command.can_run(ctx) await ctx.invoke(command, user) @commands.command( aliases=["whoami"], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="user", description="The user you want to get the information of.", type=discord.ApplicationCommandOptionType.user, required=False, ), ], ), ) async def whois(self, ctx: vbu.Context, user: discord.Member = None): """ Give you some information about a user. """ # Set up our intial vars user = user or ctx.author embed = vbu.Embed(use_random_colour=True) embed.set_author_to_user(user) # Get the user account creation time create_value = f"{discord.utils.format_dt(user.created_at)}\n{discord.utils.format_dt(user.created_at, 'R')}" embed.add_field("Account Creation Time", create_value, inline=False) # Get the user guild join time if ctx.guild: join_value = f"{discord.utils.format_dt(user.joined_at)}\n{discord.utils.format_dt(user.joined_at, 'R')}" embed.add_field("Guild Join Time", join_value, inline=False) # Set the embed thumbnail embed.set_thumbnail(user.display_avatar.with_size(1024).url) # Sick if isinstance(ctx, commands.SlashContext): return await ctx.interaction.response.send_message(embed=embed) else: return await ctx.send(embed=embed) @commands.command( application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="amount", description="The number of messages that you want to log.", type=discord.ApplicationCommandOptionType.integer, required=False, max_value=500, ), ], ), ) @commands.defer() @commands.guild_only() async def createlog(self, ctx: vbu.Context, amount: int = 100): """ Create a log of chat. """ # Make some assertions so we don't get errors elsewhere assert isinstance(ctx.channel, discord.TextChannel) assert ctx.guild # Create the data we're gonna send data = { "channel_name": ctx.channel.name, "category_name": ctx.channel.category.name if ctx.channel.category else "Uncategorized", "guild_name": ctx.guild.name, "guild_icon_url": str(ctx.guild.icon.with_format("png").with_size(512)) if ctx.guild.icon else None, } data_authors = {} data_messages = [] # Get the data from the server async for message in ctx.channel.history(limit=min([max([1, amount]), 250])): for user in message.mentions + [message.author]: data_authors[user.id] = { "username": user.name, "discriminator": user.discriminator, "avatar_url": str(user.display_avatar.with_size(512).with_format("png").url), "bot": user.bot, "display_name": user.display_name, "color": user.colour.value, } message_data = { "id": message.id, "content": message.content, "author_id": message.author.id, "timestamp": int(message.created_at.timestamp()), "attachments": [str(i.url) for i in message.attachments], } embeds = [] for i in message.embeds: embed_data: dict = i.to_dict() # type: ignore if i.timestamp: embed_data.update({'timestamp': i.timestamp.timestamp()}) embeds.append(embed_data) message_data.update({'embeds': embeds}) data_messages.append(message_data) # This takes a while async with ctx.typing(): # Send data to the API data.update({"users": data_authors, "messages": data_messages[::-1]}) async with self.bot.session.post("https://voxelfox.co.uk/discord/chatlog", json=data) as r: string = io.StringIO(await r.text()) # Output it into the chat await ctx.send(file=discord.File(string, filename=f"Logs-{int(ctx.message.created_at.timestamp())}.html")) @commands.context_command(name="Screenshot message") @commands.guild_only() async def _context_command_screenshot_message(self, ctx: vbu.Context, message: discord.Message): command = self.screenshotmessage await command.can_run(ctx) await ctx.invoke(command, user=message.author, content=message) @commands.command( aliases=["screenshotmessage"], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="user", description="The user who you want to fake a message from.", type=discord.ApplicationCommandOptionType.user, ), discord.ApplicationCommandOption( name="content", description="The content that you want in the message.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @commands.guild_only() async def fakemessage(self, ctx: vbu.Context, user: discord.Member, *, content: typing.Union[str, discord.Message]): """ Create a log of chat. """ # See if we're getting a real message or a fake one content_message = ctx.message if isinstance(content, discord.Message): content_message = content user = content.author content = content.content assert isinstance(content_message.channel, discord.TextChannel) assert content_message.guild # Create the data we're gonna send data = { "channel_name": content_message.channel.name, "category_name": content_message.channel.category.name if content_message.channel.category else "Uncategorized", "guild_name": content_message.guild.name, "guild_icon_url": str(content_message.guild.icon.with_format("png").with_size(512)) if content_message.guild.icon else None, } data_authors = {} data_authors[user.id] = { "username": user.name, "discriminator": user.discriminator, "avatar_url": str(user.display_avatar.with_size(512).with_format("png").url), "bot": user.bot, "display_name": user.display_name, "color": user.colour.value, } for i in content_message.mentions: data_authors[i.id] = { "username": i.name, "discriminator": i.discriminator, "avatar_url": str(i.display_avatar.with_size(512).with_format("png").url), "bot": i.bot, "display_name": i.display_name, "color": i.colour.value, } message_data = { "id": 69, "content": content, "author_id": user.id, "timestamp": int(discord.utils.utcnow().timestamp()), } # This takes a while async with ctx.typing(): # Send data to the API data.update({"users": data_authors, "messages": [message_data]}) async with self.bot.session.post("https://voxelfox.co.uk/discord/chatlog", json=data) as r: string = await r.text() # Remove the preamble soup = BeautifulSoup(string, "html.parser") soup.find(class_="preamble").decompose() subset = str(soup) # Screenshot it options = { "quiet": "", "enable-local-file-access": "", "width": "600", "enable-javascript": "", "javascript-delay": "1000", # "window-status": "RenderingComplete", # "debug-javascript": "", # "no-stop-slow-scripts": "", } filename = f"FakedMessage-{ctx.author.id}.png" from_string = functools.partial(imgkit.from_string, subset, filename, options=options) await self.bot.loop.run_in_executor(None, from_string) # Output it into the chat await ctx.send(file=discord.File(filename)) # And delete file await asyncio.sleep(1) await asyncio.create_subprocess_exec("rm", filename)
class TimezoneInfo(vbu.Cog): @commands.group( aliases=['tz'], application_command_meta=commands.ApplicationCommandMeta(), ) async def timezone(self, ctx: vbu.Context): """ The parent group for timezone commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @staticmethod def get_common_timezone(name) -> str: if len(name) <= 4: name = name.upper() else: name = name.title() common_timezones = { "PST": "US/Pacific", "MST": "US/Mountain", "CST": "US/Central", "EST": "US/Eastern", } if name in common_timezones: name = common_timezones[name] return name @timezone.command( name="set", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="offset", description="The timezone that you live in.", type=discord.ApplicationCommandOptionType.string, autocomplete=True, ), ], ), ) async def timezone_set(self, ctx: vbu.Context, *, offset: str = None): """ Sets and stores your UTC offset into the bot. """ # Ask them the question if offset is None: ask_message = await ctx.send(( f"Hey, {ctx.author.mention}, what timezone are you currently in? You can give its name (`EST`, `GMT`, etc) " "or you can give your continent and nearest large city (`Europe/Amsterdam`, `Australia/Sydney`, etc) - this is " "case sensitive." )) try: check = lambda m: m.author.id == ctx.author.id and m.channel.id == ctx.channel.id response_message = await self.bot.wait_for("message", check=check, timeout=30) offset = response_message.content except asyncio.TimeoutError: return await ask_message.delete() # See if it's one of the more common ones that I know don't actually exist offset = self.get_common_timezone(offset) # Try and parse the timezone name try: zone = pytz.timezone(offset) except pytz.UnknownTimeZoneError: return await ctx.send(f"I can't work out what timezone you're referring to - please run this command again to try later, or go to the website (`{ctx.clean_prefix}info`) and I can work it out automatically.") # Store it in the database async with vbu.Database() as db: await db( """INSERT INTO user_settings (user_id, timezone_name) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET timezone_name=excluded.timezone_name""", ctx.author.id, zone.zone, ) await ctx.send(f"I think your current time is **{discord.utils.utcnow().astimezone(zone).strftime('%-I:%M %p')}** - I've stored this in the database.") @commands.context_command(name="Get user's timezone") async def _context_command_timezone_get(self, ctx: vbu.SlashContext, user: discord.Member): command = self.timezone_get await command.can_run(ctx) await ctx.invoke(command, user) @timezone.command( name="get", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="target", description="The user whose timezone you want to get.", type=discord.ApplicationCommandOptionType.user, required=False, ), ], ), ) @commands.defer() async def timezone_get(self, ctx: vbu.Context, target: typing.Union[discord.Member, str] = None): """ Get the current time for a given user. """ # Check if they are a bot target = target or ctx.author target_is_timezone = False if isinstance(target, str): target_is_timezone = True target = self.get_common_timezone(target) if isinstance(target, discord.Member) and target.bot: return await ctx.send("I don't think bots have timezones...") # See if they've set a timezone if not target_is_timezone: async with vbu.Database() as db: rows = await db("SELECT timezone_name, timezone_offset FROM user_settings WHERE user_id=$1", target.id) if not rows or (rows[0]['timezone_name'] is None and rows[0]['timezone_offset'] is None): return await ctx.send(f"{target.mention} hasn't set up their timezone information! They can set it by running `{ctx.clean_prefix}timezone set`.") # Grab their current time and output if target_is_timezone: try: formatted_time = (discord.utils.utcnow().astimezone(pytz.timezone(target))).strftime('%-I:%M %p') except pytz.UnknownTimeZoneError: return await ctx.send("That isn't a valid timezone.") return await ctx.send(f"The current time in **{target}** is estimated to be **{formatted_time}**.") elif rows: if rows[0]['timezone_name']: formatted_time = (discord.utils.utcnow().astimezone(pytz.timezone(rows[0]['timezone_name']))).strftime('%-I:%M %p') else: formatted_time = (discord.utils.utcnow() + timedelta(minutes=rows[0]['timezone_offset'])).strftime('%-I:%M %p') await ctx.send(f"The current time for {target.mention} is estimated to be **{formatted_time}**.", allowed_mentions=discord.AllowedMentions.none())
class MiscCommands(vbu.Cog): def __init__(self, bot: vbu.Bot): super().__init__(bot) self.button_message_locks = collections.defaultdict(asyncio.Lock) @commands.group( aliases=['topics'], invoke_without_command=False, application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True) async def topic(self, ctx: vbu.Context): """ The parent group for the topic commands. """ async with vbu.Database() as db: rows = await db("SELECT * FROM topics ORDER BY RANDOM() LIMIT 1") if not rows: return await ctx.send( "There aren't any topics set up in the database for this bot :<" ) return await ctx.send(rows[0]['topic']) @topic.command( name="get", application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True) async def topic_get(self, ctx: vbu.Context): """ Gives you a conversation topic. """ await self.topic(ctx) # @topic.command( # name="add", # application_command_meta=commands.ApplicationCommandMeta( # options=[ # discord.ApplicationCommandOption( # name="topic", # description="", # type=discord.ApplicationCommandOptionType.string, # ), # ], # ), # ) # @vbu.checks.is_bot_support() # @commands.bot_has_permissions(send_messages=True) # async def topic_add(self, ctx: vbu.Context, *, topic: str): # """ # Add a new topic to the database. # """ # async with vbu.Database() as db: # await db("INSERT INTO topics VALUES ($1)", topic) # return await ctx.send("Added to database.") @commands.command( application_command_meta=commands.ApplicationCommandMeta()) @commands.bot_has_permissions(send_messages=True) async def coinflip(self, ctx: vbu.Context): """ Flips a coin. """ coin = ["Heads", "Tails"] return await ctx.send(random.choice(coin)) # @commands.command(aliases=['http']) # @commands.cooldown(1, 5, commands.BucketType.channel) # async def httpcat(self, ctx: vbu.Context, errorcode: str): # """ # Gives you a cat based on an HTTP error code. # """ # standard_errorcodes = [error.value for error in http.HTTPStatus] # if errorcode in ('random', 'rand', 'r'): # errorcode = random.choice(standard_errorcodes) # else: # try: # errorcode = int(errorcode) # except ValueError: # return ctx.channel.send('Converting to "int" failed for parameter "errorcode".') # await ctx.trigger_typing() # headers = {"User-Agent": self.bot.user_agent} # async with self.bot.session.get(f"https://http.cat/{errorcode}", headers=headers) as r: # if r.status == 404: # if errorcode not in standard_errorcodes: # await ctx.send("That HTTP code doesn't exist.") # else: # await ctx.send('Image for HTTP code not found on provider.') # return # if r.status != 200: # await ctx.send(f'Something went wrong, try again later. ({r.status})') # return # with vbu.Embed(use_random_colour=True) as embed: # embed.set_image(url=f'https://http.cat/{errorcode}') # await ctx.send(embed=embed) # @commands.command() # @commands.cooldown(1, 5, commands.BucketType.channel) # async def httpdog(self, ctx: vbu.Context, errorcode: str): # """ # Gives you a dog based on an HTTP error code. # """ # standard_errorcodes = [error.value for error in http.HTTPStatus] # if errorcode in ('random', 'rand', 'r'): # errorcode = random.choice(standard_errorcodes) # else: # try: # errorcode = int(errorcode) # except ValueError: # return ctx.channel.send('Converting to "int" failed for parameter "errorcode".') # await ctx.trigger_typing() # headers = {"User-Agent": self.bot.user_agent} # async with self.bot.session.get( # f"https://httpstatusdogs.com/img/{errorcode}.jpg", headers=headers, allow_redirects=False) as r: # if str(r.status)[0] != "2": # if errorcode not in standard_errorcodes: # await ctx.send("That HTTP code doesn't exist.") # else: # await ctx.send('Image for HTTP code not found on provider.') # return # with vbu.Embed(use_random_colour=True) as embed: # embed.set_image(url=f'https://httpstatusdogs.com/img/{errorcode}.jpg') # await ctx.send(embed=embed, wait=False) @commands.command( aliases=['color'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="colour", description="The colour that you want to post.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def colour( self, ctx: vbu.Context, *, colour: typing.Union[vbu.converters.ColourConverter, discord.Colour, discord.Role, discord.Member], ): """ Get you a colour. """ # https://www.htmlcsscolor.com/preview/gallery/5dadec.png if isinstance(colour, discord.Role): colour = colour.colour elif isinstance(colour, discord.Member): try: colour = [i for i in colour.roles if i.colour.value > 0][-1].colour except IndexError: colour = discord.Colour(0) hex_colour = colour.value with vbu.Embed(colour=hex_colour, title=f"#{hex_colour:0>6X}") as embed: embed.set_image( url= f"https://www.htmlcsscolor.com/preview/gallery/{hex_colour:0>6X}.png" ) await ctx.send(embed=embed) @commands.command( aliases=['disconnectvc', 'emptyvc'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="channel", description="The VC that you want to clear.", type=discord.ApplicationCommandOptionType.channel, channel_types=[discord.ChannelType.voice], ), ], ), ) @commands.has_guild_permissions(move_members=True) @commands.bot_has_guild_permissions(move_members=True) @commands.bot_has_permissions(send_messages=True) async def clearvc(self, ctx: vbu.Context, channel: discord.VoiceChannel): """ Removes all the people from a given VC. """ if not channel.members: return await ctx.send( "There are no people in that VC for me to remove.") member_count = len(channel.members) await ctx.defer() for member in channel.members: try: await member.edit(voice_channel=None) except discord.Forbidden: return await ctx.send( "I don't have permission to remove members from that channel." ) return await ctx.send(f"Dropped {member_count} members from the VC.")
class RunescapeCommands(vbu.Cog): def __init__(self, bot): super().__init__(bot) self.item_ids_path = Path().parent.joinpath('config').joinpath( 'osrs-item-ids.json') with open(self.item_ids_path) as item_ids_file: self.item_ids = json.load(item_ids_file) @staticmethod def rs_notation_to_int(value_str: str) -> int: """ Change a value string ("1.2m") into an int (1200000) https://github.com/JMcB17/osrs-blast-furnace-calc """ multipliers = { 'k': 10**3, 'm': 10**6, 'b': 10**9, } value_str = value_str.replace(',', '').strip() for multi, value in multipliers.items(): if value_str.endswith(multi): value_str = value_str.rstrip(multi) value = int(float(value_str) * value) break else: value = int(value_str) return value async def get_item_details_by_id(self, item_id: int) -> dict: """ Return the JSON response from the rs API given an item's Runescape ID. """ # Send our web request url = API_BASE_URL + 'api/catalogue/detail.json' params = { 'item': item_id, } headers = { "User-Agent": self.bot.user_agent, } async with self.bot.session.get(url, params=params, headers=headers) as response: # The Runescape API doesn't set a json header, so aiohttp complains about it. # We can just say to not check the content type. # https://github.com/aio-libs/aiohttp/blob/8c82ba11b9e38851d75476d261a1442402cc7592/aiohttp/web_request.py#L664-L681 item = await response.json(content_type=None) # revolver ocelot (revolver ocelot) item = item['item'] return item async def parse_item_value( self, item: dict, return_int: bool = True) -> typing.Union[int, str]: """ Parse the value of an item from the JSON response from the rs API. """ value = item['current']['price'] if isinstance(value, str): value = value.strip() if return_int: value = self.rs_notation_to_int(value) else: value = str(value) return value @commands.command( aliases=['ge'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="item", description="The item that you want to search.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) async def grandexchange(self, ctx: vbu.Context, *, item: str): """ Get the value of an item on the grand exchange (OSRS). """ async with ctx.typing(): if item.lower() in ['random']: item_id = random.choice(list(self.item_ids.values())) else: item = item.capitalize() item_id = self.item_ids.get(item) if item_id: item_dict = await self.get_item_details_by_id(item_id) item_value = await self.parse_item_value(item_dict, return_int=False) name = item_dict['name'] item_page_url = API_BASE_URL + f"a=373/{name.replace(' ', '+')}/viewitem?obj={item_id}" with vbu.Embed() as embed: embed.set_author(name=name, url=item_page_url, icon_url=item_dict['icon']) embed.set_thumbnail(url=item_dict['icon_large']) embed.add_field('Value', f'{item_value} coins', inline=False) embed.add_field(f'Examine {name}', item_dict['description'], inline=False) embed.add_field('Members', MEMBERS_MAPPING[item_dict['members']], inline=False) return await ctx.send(embed=embed) else: return await ctx.send('Item not found')
class DNDCommands(vbu.Cog[Bot]): DICE_REGEX = re.compile( r"^(?P<count>\d+)?[dD](?P<type>\d+) *(?P<modifier>(?P<modifier_parity>[+-]) *(?P<modifier_amount>\d+))?$" ) ATTRIBUTES = { "strength": "STR", "dexterity": "DEX", "constitution": "CON", "intelligence": "INT", "wisdom": "WIS", "charisma": "CHR", } @commands.command( aliases=['roll'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="dice", type=discord.ApplicationCommandOptionType.string, description= "The die (in form 'XdY+Z', eg '5d6+2') that you want to roll.", ), ], ), ) @commands.bot_has_permissions(send_messages=True) async def dice(self, ctx: commands.Context, *, dice: str): """ Rolls a dice for you. """ # Validate the dice if not dice: raise vbu.errors.MissingRequiredArgumentString(dice) match = self.DICE_REGEX.search(dice) if not match: raise commands.BadArgument( "Your dice was not in the format `AdB+C`.") # Roll em dice_count = int(match.group("count") or 1) dice_type = int(match.group("type")) modifier = int((match.group("modifier") or "+0").replace(" ", "")) rolls = [random.randint(1, dice_type) for _ in range(dice_count)] total = sum(rolls) + modifier dice_string = f"{dice_count}d{dice_type}{modifier:+}" if not modifier: dice_string = dice_string[:-2] # Get formatted output if dice_count > 1 or modifier: equals_string = f"{sum(rolls)} {'+' if modifier > 0 else '-'} {abs(modifier)}" if modifier: text = f"Total **{total:,}** ({dice_string})\n({', '.join([str(i) for i in rolls])}) = {equals_string}" else: text = f"Total **{total:,}** ({dice_string})\n({', '.join([str(i) for i in rolls])}) = {equals_string}" else: text = f"Total **{total}** ({dice_string})" # And output if isinstance(ctx, commands.SlashContext): await ctx.interaction.response.send_message(text) else: await ctx.send(text) async def send_web_request(self, resource: str, item: str) -> typing.Optional[dict]: """ Send a web request to the dnd5eapi website. """ url = f"https://www.dnd5eapi.co/api/{resource}/{quote(item.lower().replace(' ', '-'))}/" headers = {"User-Agent": self.bot.user_agent} async with self.bot.session.get(url, headers=headers) as r: v = await r.json() if v.get("error"): return None return v @staticmethod def group_field_descriptions( embed: discord.Embed, field_name: str, input_list: typing.List[_DNDMonsterAction], ) -> None: """ Add fields grouped to the embed character limit. """ original_field_name = field_name joiner = "\n" action_text = [ f"**{i['name']}**{joiner}{i['desc']}" for i in input_list ] add_text = "" for text in action_text: if len(add_text) + len(text) + 1 > 1024: embed.add_field( name=field_name, value=add_text, inline=False, ) field_name = f"{original_field_name} Continued" add_text = "" add_text += joiner + text if add_text: embed.add_field( name=field_name, value=add_text, inline=False, ) @commands.group( aliases=["d&d"], application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True) async def dnd(self, ctx: commands.Context): """ The parent group for the D&D commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @dnd.command( name="spell", aliases=["spells"], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="spell_name", description= "The name of the spell that you want to get the information of.", type=discord.ApplicationCommandOptionType.string, autocomplete=True, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) async def dnd_spell(self, ctx: commands.Context, *, spell_name: str): """ Gives you information on a D&D spell. """ # Get our data data = await self.send_web_request("spells", spell_name) if not data: return await ctx.send( "I couldn't find any information for that spell.") # Make an embed embed = vbu.Embed( use_random_colour=True, title=data['name'], description=data['desc'][0], ).add_field( name="Casting Time", value=data['casting_time'], ).add_field( name="Range", value=data['range'], ).add_field( name="Components", value=', '.join(data['components']), ).add_field( name="Material", value=data.get('material', 'N/A'), ).add_field( name="Duration", value=data['duration'], ).add_field( name="Classes", value=', '.join([i['name'] for i in data['classes']]), ).add_field( name="Ritual", value=data['ritual'], ).add_field( name="Concentration", value=data['concentration'], ) if data.get('higher_level'): embed.add_field( name="Higher Level", value="\n".join(data['higher_level']), inline=False, ) elif data.get('damage'): text = "" if data['damage'].get('damage_at_character_level'): text += "\nCharacter level " + ", ".join([ f"{i}: {o}" for i, o in data['damage'] ['damage_at_character_level'].items() ]) if data['damage'].get('damage_at_slot_level'): text += "\nSlot level " + ", ".join([ f"{i}: {o}" for i, o in data['damage']['damage_at_slot_level'].items() ]) embed.add_field( name="Damage", value=text.strip(), inline=False, ) # And send return await ctx.send(embed=embed) @functools.cached_property def all_spells(self) -> typing.List[str]: with open("cogs/data/dnd_spells.txt") as a: return a.read().strip().split("\n") @dnd_spell.autocomplete async def dnd_spell_autocomplete(self, ctx: commands.SlashContext, interaction: discord.Interaction[None]): """ Fuzzy match what the user input and the list of all spells. """ # Get what the user gave us assert interaction.options is not None user_input = interaction.options[0].options[0].value assert user_input # Determine what's closest to what they said fuzzed = [( i, fuzz.ratio(i, user_input), ) for i in self.all_spells if user_input.casefold() in i.casefold()] fuzzed.sort(key=operator.itemgetter(1), reverse=True) # And give them the top results await interaction.response.send_autocomplete([ discord.ApplicationCommandOptionChoice(name=i, value=i) for i, _ in fuzzed[:25] ]) @dnd.command( name="monster", aliases=["monsters"], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="monster_name", description= "The monster that you want to get the information of.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) async def dnd_monster(self, ctx: commands.Context, *, monster_name: str): """ Gives you information on a D&D monster. """ # Get our data data = await self.send_web_request("monsters", monster_name) if not data: return await ctx.send( "I couldn't find any information for that monster.") # Make an embed embed = vbu.Embed( use_random_colour=True, title=data['name'], description="\n".join([ f"{data['size'].capitalize()} | {data['type']} | {data['hit_points']:,} ({data['hit_dice']}) HP | {data['xp']:,} XP", ", ".join( [f"{o} {data[i]}" for i, o in self.ATTRIBUTES.items()]), ])).add_field( name="Proficiencies", value=", ".join([ f"{i['proficiency']['name']} {i['value']}" for i in data['proficiencies'] ]) or "None", ).add_field( name="Damage Vulnerabilities", value="\n".join(data['damage_vulnerabilities']).capitalize() or "None", ).add_field( name="Damage Resistances", value="\n".join(data['damage_resistances']).capitalize() or "None", ).add_field( name="Damage Immunities", value="\n".join(data['damage_immunities']).capitalize() or "None", ).add_field( name="Condition Immunities", value="\n".join( [i['name'] for i in data['condition_immunities']]).capitalize() or "None", ).add_field( name="Senses", value="\n".join([ f"{i.replace('_', ' ').capitalize()} {o}" for i, o in data['senses'].items() ]) or "None", ) self.group_field_descriptions(embed, "Actions", data['actions']) self.group_field_descriptions(embed, "Legendary Actions", data.get('legendary_actions', list())) if data.get('special_abilities'): embed.add_field( name="Special Abilities", value="\n".join([ f"**{i['name']}**\n{i['desc']}" for i in data['special_abilities'] if i['name'] != 'Spellcasting' ]) or "None", inline=False, ) spellcasting = [ i for i in data.get('special_abilities', list()) if i['name'] == 'Spellcasting' ] if spellcasting: spellcasting = spellcasting[0] embed.add_field( name="Spellcasting", value=spellcasting['desc'].replace('\n\n', '\n'), inline=False, ) # And send return await ctx.send(embed=embed) @dnd.command( name="condition", aliases=["conditions"], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="condition_name", description= "The condition that you want to get the information of.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) async def dnd_condition(self, ctx: commands.Context, *, condition_name: str): """ Gives you information on a D&D condition. """ # Get our data async with ctx.typing(): data = await self.send_web_request("conditions", condition_name) if not data: return await ctx.send( "I couldn't find any information for that condition.") # Make an embed embed = vbu.Embed( use_random_colour=True, title=data['name'], description="\n".join(data['desc']), ) # And send return await ctx.send(embed=embed) @dnd.command( name="class", aliases=["classes"], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="class_name", description="The class that you want to get the informaton of.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @commands.bot_has_permissions(send_messages=True, embed_links=True) async def dnd_class(self, ctx: commands.Context, *, class_name: str): """ Gives you information on a D&D class. """ # Get our data async with ctx.typing(): data = await self.send_web_request("classes", class_name) if not data: return await ctx.send( "I couldn't find any information for that class.") # Make embed embed = vbu.Embed( use_random_colour=True, title=data['name'], ).add_field( "Proficiencies", ", ".join([i['name'] for i in data['proficiencies']]), ).add_field( "Saving Throws", ", ".join([i['name'] for i in data['saving_throws']]), ).add_field( "Starting Equipment", "\n".join([ f"{i['quantity']}x {i['equipment']['name']}" for i in data['starting_equipment'] ]), ) # And send return await ctx.send(embed=embed)
class GoogleCommands(vbu.Cog): def get_search_page(self, query: str, num: int, image: bool = False): """ Get a number of results from Google """ async def wrapper(page_number: int): params = { 'key': self.bot.config['api_keys']['google']['api_key'], 'cx': self.bot.config['api_keys']['google']['search_engine_id'], 'num': num, 'q': query, 'safe': 'active', 'start': num * page_number, } if image: params.update({'searchType': 'image'}) formatter = lambda d: (d['title'][:256], d['link']) else: formatter = lambda d: ( d['title'][:256], f"[{d['displayLink']}]({d['link']}) - {d['snippet'].replace(ENDL, ' ')}" ) async with self.bot.session.get( "https://customsearch.googleapis.com/customsearch/v1", params=params) as r: data = await r.json() ENDL = '\n' output_data = [] for d in data.get('items', list()): output_data.append(formatter(d)) if not output_data: raise StopAsyncIteration() return output_data return wrapper @commands.group( invoke_without_command=True, aliases=['search'], application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id') @vbu.checks.is_config_set('api_keys', 'google', 'api_key') async def google(self, ctx: vbu.Context): """ The parent group for the google commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @google.command( name="search", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="query", description="The text that you want to search.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id') @vbu.checks.is_config_set('api_keys', 'google', 'api_key') async def google_search(self, ctx: vbu.Context, *, query: str): """ Search a query on Google. """ if query.startswith("-"): raise vbu.errors.MissingRequiredArgumentString("query") def formatter(menu, data): embed = vbu.Embed(use_random_colour=True) for d in data: embed.add_field(*d, inline=False) embed.set_footer(f"Page {menu.current_page + 1}/{menu.max_pages}") return embed await vbu.Paginator(self.get_search_page(query, 3), formatter=formatter).start(ctx) @google.command( name='images', aliases=['image', 'i'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="query", description="The text that you want to search.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.has_permissions(embed_links=True) @commands.bot_has_permissions(send_messages=True, embed_links=True) @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id') @vbu.checks.is_config_set('api_keys', 'google', 'api_key') async def google_image(self, ctx: vbu.Context, *, query: str): """ Search a query on Google Images. """ if query.startswith("-"): raise vbu.errors.MissingRequiredArgumentString("query") def formatter(menu, data): return vbu.Embed( use_random_colour=True, title=data[0][0]).set_image(data[0][1]).set_footer( f"Page {menu.current_page + 1}/{menu.max_pages}") await vbu.Paginator(self.get_search_page(query, 1, True), formatter=formatter).start(ctx)
class QuoteCommands(vbu.Cog): IMAGE_URL_REGEX = re.compile( r"(http(?:s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg|webp)") QUOTE_SEARCH_CHARACTER_CUTOFF = 100 async def get_quote_messages(self, ctx: vbu.Context, messages: typing.List[discord.Message], *, allow_self_quote: bool = False) -> dict: """ Gets the messages that the user has quoted, returning a dict with keys `success` (bool) and `message` (str or voxelbotutils.Embed). If `success` is `False`, then the resulting `message` can be directly output to the user, and if it's `True` then we can go ahead with the message save flowthrough. """ # Make sure they have a quote channel assert ctx.guild if self.bot.guild_settings[ctx.guild.id].get( 'quote_channel_id') is None: func = "You don't have a quote channel set!" return {'success': False, 'message': func} # Make sure a message was passed if not messages: if ctx.message.reference is not None: message_from_reply = await ctx.fetch_message( ctx.message.reference.message_id) messages = [message_from_reply] else: return { 'success': False, 'message': "I couldn't find any references to messages in your command call." } # Recreate the message list without duplicates unique_messages = [] unique_message_ids = set() for i in messages: if i.id not in unique_message_ids: unique_messages.append(i) unique_message_ids.add(i.id) messages = unique_messages # Make sure they're all sent as a reasonable time apart quote_is_url = False messages = sorted(messages, key=lambda m: m.created_at) for i, o in zip(messages, messages[1:]): if o is None: break if (o.created_at - i.created_at).total_seconds() > 3 * 60: return { 'success': False, 'message': "Those messages are too far apart to quote together." } if not i.content or i.attachments: if len(i.attachments) == 0: return { 'success': False, 'message': "Embeds can't be quoted." } if i.attachments: return { 'success': False, 'message': "You can't quote multiple messages when quoting images." } # Validate the message content for message in messages: if (quote_is_url and message.content) or ( message.content and message.attachments and message.content != message.attachments[0].url): return { 'success': False, 'message': "You can't quote both messages and images." } elif message.embeds and getattr(message.embeds[0].thumbnail, "url", None) != message.content: return {'success': False, 'message': "You can't quote embeds."} elif len(message.attachments) > 1: return { 'success': False, 'message': "Multiple images can't be quoted." } elif message.attachments: if self.IMAGE_URL_REGEX.search( message.attachments[0].url) is None: return { 'success': False, 'message': "The attachment in that image isn't a valid image URL." } message.content = message.attachments[0].url quote_is_url = True # Validate input timestamp = discord.utils.naive_dt(messages[0].created_at) user = messages[0].author text = '\n'.join([m.content for m in messages]) if len(set([i.author.id for i in messages])) != 1: return { 'success': False, 'message': "You can only quote one person at a time." } # Make sure they're not quoting themself if there are no reactions needed message_author = messages[0].author reactions_needed = self.bot.guild_settings[ ctx.guild.id]['quote_reactions_needed'] if ctx.author.id in self.bot.owner_ids: pass elif ctx.author.id == message_author.id and ( reactions_needed or allow_self_quote is False): return { 'success': False, 'message': "You can't quote yourself when there's no vote :/" } # Return an embed with vbu.Embed(use_random_colour=True) as embed: embed.set_author_to_user(user) if quote_is_url: embed.set_image(text) else: embed.description = text embed.timestamp = timestamp return { 'success': True, 'message': embed, 'user': user, 'timestamp': timestamp } @commands.group( invoke_without_command=True, application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True, embed_links=True, add_reactions=True) @commands.guild_only() async def quote(self, ctx: vbu.Context, messages: commands.Greedy[discord.Message]): """ Quotes a user's message to the guild's quote channel. """ # Make sure no subcommand is passed if ctx.invoked_subcommand is not None: return response = await self.get_quote_messages(ctx, messages) # Make embed if response['success'] is False: return await ctx.send(response['message']) embed = response['message'] user = response['user'] timestamp = response['timestamp'] # See if we should bother saving it reactions_needed = self.bot.guild_settings[ ctx.guild.id]['quote_reactions_needed'] ask_to_save_message = await ctx.send( f"Should I save this quote? If I receive {reactions_needed} positive reactions in the next 60 seconds, the quote will be saved.", embed=embed, ) self.bot.loop.create_task( ask_to_save_message.add_reaction("\N{THUMBS UP SIGN}")) self.bot.loop.create_task( ask_to_save_message.add_reaction("\N{THUMBS DOWN SIGN}")) await asyncio.sleep(60) # Get the message again so we can refresh the reactions try: ask_to_save_message_again = await ask_to_save_message.channel.fetch_message( ask_to_save_message.id) reaction_count = sum([ i.count if str(i.emoji) == "\N{THUMBS UP SIGN}" else -i.count if str(i.emoji) == "\N{THUMBS DOWN SIGN}" else 0 for i in ask_to_save_message_again.reactions ]) except discord.HTTPException: return try: await ask_to_save_message.delete() except discord.HTTPException: pass if reaction_count < reactions_needed: return await ctx.send( f"_Not_ saving the quote asked by {ctx.author.mention} - not enough reactions received." ) # If we get here, we can save to db quote_id = create_id() # See if they have a quotes channel quote_channel_id = self.bot.guild_settings[ctx.guild.id].get( 'quote_channel_id') embed.set_footer(text=f"Quote ID {quote_id.upper()}") posted_message = None if quote_channel_id: channel = self.bot.get_channel(quote_channel_id) try: posted_message = await channel.send(embed=embed) except (discord.Forbidden, AttributeError): pass if quote_channel_id is None or posted_message is None: return await ctx.send( "I couldn't send your quote into the quote channel.") # And save it to the database async with vbu.Database() as db: await db( """INSERT INTO user_quotes (quote_id, guild_id, channel_id, message_id, user_id, timestamp, quoter_id) VALUES ($1, $2, $3, $4, $5, $6, $7)""", quote_id, ctx.guild.id, posted_message.channel.id, posted_message.id, user.id, timestamp, ctx.author.id, ) # Output to user await ctx.send( f"{ctx.author.mention}'s quote request saved with ID `{quote_id.upper()}`", embed=embed) @commands.context_command(name="Quote message") async def _context_command_quote_create(self, ctx: vbu.Context, message: discord.Message): command = self.quote_create await command.can_run(ctx) await ctx.invoke(command, message) @quote.command(name="create") @commands.bot_has_permissions(send_messages=True, embed_links=True, add_reactions=True) @commands.guild_only() async def quote_create(self, ctx: vbu.Context, message: discord.Message): """ Quotes a user's message to the guild's quote channel. """ await self.quote(ctx, [message]) @quote.command(name="force") @commands.has_guild_permissions(manage_guild=True) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def quote_force(self, ctx: vbu.Context, messages: commands.Greedy[discord.Message]): """ Quotes a user's message to the guild's quote channel. """ # Make sure no subcommand is passed if ctx.invoked_subcommand is not None: return response = await self.get_quote_messages(ctx, messages, allow_self_quote=True) # Make embed if response['success'] is False: return await ctx.send(response['message']) embed = response['message'] user = response['user'] timestamp = response['timestamp'] # See if they have a quotes channel quote_channel_id = self.bot.guild_settings[ctx.guild.id].get( 'quote_channel_id') quote_id = create_id() embed.set_footer(text=f"Quote ID {quote_id.upper()}") posted_message = None if quote_channel_id: channel = self.bot.get_channel(quote_channel_id) try: posted_message = await channel.send(embed=embed) except (discord.Forbidden, AttributeError): pass if quote_channel_id is None or posted_message is None: return await ctx.send( "I couldn't send your quote into the quote channel.") # And save it to the database async with vbu.Database() as db: await db( """INSERT INTO user_quotes (quote_id, guild_id, channel_id, message_id, user_id, timestamp, quoter_id) VALUES ($1, $2, $3, $4, $5, $6, $7)""", quote_id, ctx.guild.id, posted_message.channel.id, posted_message.id, user.id, timestamp.replace(tzinfo=None), ctx.author.id, ) # Output to user await ctx.send( f"{ctx.author.mention}'s quote saved with ID `{quote_id.upper()}`", embed=embed) @quote.command( name="get", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="identifier", description="The ID of the quote that you want to get.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def quote_get(self, ctx: vbu.Context, identifier: str): """ Gets a quote from the guild's quote channel. """ # Get quote from database async with vbu.Database() as db: quote_rows = await db( """SELECT user_quotes.quote_id as quote_id, user_id, channel_id, message_id FROM user_quotes LEFT JOIN quote_aliases ON user_quotes.quote_id=quote_aliases.quote_id WHERE user_quotes.quote_id=$1 OR quote_aliases.alias=$1""", identifier.lower(), ) if not quote_rows: return await ctx.send( f"There's no quote with the identifier `{identifier.upper()}`.", allowed_mentions=discord.AllowedMentions.none(), ) # Get the message data = quote_rows[0] if data['channel_id'] is None: return await ctx.send( "There's no quote channel set for that quote.") channel = self.bot.get_channel(data['channel_id']) if channel is None: return await ctx.send("I wasn't able to get your quote channel.") try: message = await channel.fetch_message(data['message_id']) assert message is not None except (AssertionError, discord.HTTPException): return await ctx.send("I wasn't able to get your quote message.") # try to refresh the user name and icon of the embed by getting the user from the user ID in the DB quote_embed = message.embeds[0] quote_author = self.bot.get_user(data['user_id']) if quote_author: quote_embed.set_author(name=quote_author.display_name, icon_url=quote_author.display_avatar.url) # Output to user return await ctx.send(embed=quote_embed) @quote.command( name="random", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="user", description="The user whose quotes you want to search.", type=discord.ApplicationCommandOptionType.user, required=False, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def quote_random(self, ctx: vbu.Context, user: discord.Member = None): """ Gets a random quote for a given user. """ # Get quote from database user = user or ctx.author async with vbu.Database() as db: quote_rows = await db( """SELECT quote_id as quote_id, user_id, channel_id, message_id FROM user_quotes WHERE user_id=$1 AND guild_id=$2 ORDER BY RANDOM() LIMIT 1""", user.id, ctx.guild.id, ) if not quote_rows: return await ctx.send( f"{user.mention} has no available quotes.", allowed_mentions=discord.AllowedMentions.none()) # Get the message data = quote_rows[0] if data['channel_id'] is None: self.logger.info(f"Deleting legacy quote - {data['quote_id']}") async with vbu.Database() as db: await db("DELETE FROM user_quotes WHERE quote_id=$1", data['quote_id']) return await ctx.reinvoke() channel = self.bot.get_channel(data['channel_id']) if channel is None: self.logger.info( f"Deleting quote from deleted channel - {data['quote_id']}") async with vbu.Database() as db: await db("DELETE FROM user_quotes WHERE quote_id=$1", data['quote_id']) return await ctx.reinvoke() try: message = await channel.fetch_message(data['message_id']) assert message is not None except (AssertionError, discord.HTTPException): self.logger.info( f"Deleting quote from deleted message - {data['quote_id']}") async with vbu.Database() as db: await db("DELETE FROM user_quotes WHERE quote_id=$1", data['quote_id']) return await ctx.reinvoke() # Output to user quote_embed = message.embeds[0] quote_author = self.bot.get_user(data['user_id']) if quote_author: quote_embed.set_author(name=quote_author.display_name, icon_url=quote_author.display_avatar.url) return await ctx.send(embed=quote_embed) # @quote.group(name="alias", invoke_without_command=True) # @commands.guild_only() # @commands.has_guild_permissions(manage_guild=True) # @commands.bot_has_permissions(send_messages=True) # async def quote_alias(self, ctx: vbu.Context, quote_id: commands.clean_content, alias: commands.clean_content): # """ # Adds an alias to a quote. # """ # # Grab data from db # async with vbu.Database() as db: # rows = await db("SELECT * FROM user_quotes WHERE quote_id=$1 AND guild_id=$2", quote_id.lower(), ctx.guild.id) # if not rows: # return await ctx.send(f"There's no quote with the ID `{quote_id.upper()}`.") # # Insert alias into db # async with vbu.Database() as db: # rows = await db("SELECT * FROM quote_aliases WHERE alias=$1", alias) # if rows: # return await ctx.send(f"The alias `{alias}` is already being used.") # await db("INSERT INTO quote_aliases (quote_id, alias) VALUES ($1, $2)", quote_id.lower(), alias.lower()) # await ctx.send(f"Added the alias `{alias.upper()}` to quote ID `{quote_id.upper()}`.") # @quote.command(name="list") # @commands.guild_only() # @commands.has_guild_permissions(manage_guild=True) # @commands.bot_has_permissions(send_messages=True) # async def quote_list(self, ctx: vbu.Context, user: discord.Member=None): # """ # List the IDs of quotes for a user. # """ # # Grab data from db # user = user or ctx.author # async with vbu.Database() as db: # rows = await db("SELECT quote_id FROM user_quotes WHERE user_id=$1 AND guild_id=$2", user, ctx.guild.id) # if not rows: # embed = vbu.Embed( # use_random_colour=True, description="This user has no quotes.", # ).set_author_to_user(user) # return await ctx.send(embed=embed) # embed = vbu.Embed( # use_random_colour=True, description="\n".join([i['quote_id'] for i in rows[:50]]), # ).set_author_to_user(user) # return await ctx.send(embed=embed) # @quote_alias.command(name="remove", aliases=["delete"]) # @commands.guild_only() # @commands.has_guild_permissions(manage_guild=True) # @commands.bot_has_permissions(send_messages=True) # async def quote_alias_remove(self, ctx: vbu.Context, alias: commands.clean_content): # """ # Deletes an alias from a quote. # """ # # Grab data from db # async with vbu.Database() as db: # quote_rows = await db( # """SELECT user_quotes.quote_id as quote_id, user_id, channel_id, message_id FROM user_quotes LEFT JOIN # quote_aliases ON user_quotes.quote_id=quote_aliases.quote_id # WHERE quote_aliases.alias=$1 AND guild_id=$2""", # alias.lower(), ctx.guild.id # ) # if not quote_rows: # return await ctx.send(f"There's no quote with the alias `{alias.upper()}`.") # await db("DELETE FROM quote_aliases WHERE alias=$1", alias.lower()) # return await ctx.send(f"Deleted alias `{alias.upper()}`.") @quote.command( name="delete", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="quote_id", description="The ID of the quote that you want to delete.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.guild_only() @commands.has_permissions(manage_messages=True) @commands.bot_has_permissions(send_messages=True) async def quote_delete(self, ctx: vbu.Context, quote_id: str): """ Deletes a quote from your server. """ # quote_ids = [i.lower() for i in quote_ids] quote_ids = [quote_id.lower()] quote_channel_id = self.bot.guild_settings[ctx.guild.id].get( 'quote_channel_id') if quote_channel_id: quote_channel = self.bot.get_channel(quote_channel_id) try: async for message in quote_channel.history(limit=150): if not message.author.id == ctx.guild.me.id: continue if not message.embeds: continue embed = message.embeds[0] if not embed.footer: continue footer_text = embed.footer.text if not footer_text: continue if not footer_text.startswith("Quote ID"): continue message_quote_id = footer_text.split(' ')[2].lower() if message_quote_id in quote_ids: try: await message.delete() except discord.HTTPException: pass except (discord.HTTPException, AttributeError) as e: await ctx.send(e) async with vbu.Database() as db: await db( "DELETE FROM user_quotes WHERE quote_id=ANY($1) AND guild_id=$2", quote_ids, ctx.guild.id) return await ctx.send("Deleted quote(s).")
class GithubCommands(vbu.Cog[Bot]): GIT_ISSUE_OPEN_EMOJI = "<:github_issue_open:817984658456707092>" GIT_ISSUE_CLOSED_EMOJI = "<:github_issue_closed:817984658372689960>" GIT_PR_OPEN_EMOJI = "<:github_pr_open:817986200618139709>" GIT_PR_CLOSED_EMOJI = "<:github_pr_closed:817986200962072617>" GIT_PR_CHANGES_EMOJI = "<:github_changes_requested:819115452948938772>" def __init__(self, bot: Bot): super().__init__(bot) GitRepo.bot = bot async def increase_repo_usage_counter(self, user: typing.Union[discord.User, discord.Member], repo: GitRepo): async with vbu.Database() as db: await db( """INSERT INTO github_repo_uses (user_id, owner, repo, host, uses) VALUES ($1, $2, $3, $4, 1) ON CONFLICT (user_id, owner, repo, host) DO UPDATE SET uses=github_repo_uses.uses+excluded.uses""", user.id, repo.owner, repo.repo, repo.host, ) @vbu.Cog.listener() async def on_message(self, message: discord.Message): """ Sends GitHub/Lab links if a message sent in the server matches the format `gh/user/repo`. """ if message.author.bot: return if (await self.bot.get_context(message)).command is not None: return # Find matches in the message m = re.finditer( ( r'(?:\s|^)(?P<ident>g[hl])/(?P<url>(?P<user>[a-zA-Z0-9_-]{1,255})/(?P<repo>[a-zA-Z0-9_-]{1,255}))' r'(?:[#!]?(?P<issue>\d+?))?(?:\s|$)' ), message.content, ) n = re.finditer( r'(?:\s|^)(?P<ident>g[hl]) (?P<alias>\S{1,255})(?: [#!]?(?P<issue>\d+?))?(?:\s|$)', message.content, ) # Dictionary of possible Git() links git_dict = { "gh": "hub", "gl": "lab", } # Add the url of each matched link to the final output sendable = "" for i in m: url = i.group("url") ident = i.group("ident") issue = i.group("issue") url = f"https://git{git_dict[ident]}.com/{url}" if issue: if ident == "gh": url = f"{url}/issues/{issue}" elif ident == "gl": url = f"{url}/-/issues/{issue}" sendable += f"<{url}>\n" if n: async with vbu.Database() as db: for i in n: issue = i.group("issue") rows = await db("SELECT * FROM github_repo_aliases WHERE alias=$1", i.group("alias")) if rows: url = f"https://{rows[0]['host'].lower()}.com/{rows[0]['owner']}/{rows[0]['repo']}" if issue: if rows[0]['host'] == "Github": url = f"{url}/issues/{issue}" elif rows[0]['host'] == "Gitlab": url = f"{url}/-/issues/{issue}" sendable += f"<{url}>\n" # Send the GitHub links if there's any output if sendable: await message.channel.send(sendable, allowed_mentions=discord.AllowedMentions.none()) @commands.group(application_command_meta=commands.ApplicationCommandMeta()) async def repoalias(self, ctx: vbu.Context): """ The parent command for handling git repo aliases. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) @repoalias.command( name="add", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="alias", description="The user-readable name of the repo.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="repo", description="A link to the repo that you want to alias.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True) async def repoalias_add(self, ctx: vbu.Context, alias: str, repo: GitRepo): """ Add a Github repo alias to the database. """ async with vbu.Database() as db: try: await db( """INSERT INTO github_repo_aliases (alias, owner, repo, host, added_by) VALUES (LOWER($1), $2, $3, $4, $5)""", alias, repo.owner, repo.repo, repo.host, ctx.author.id, ) except asyncpg.UniqueViolationError: data = await db("SELECT * FROM github_repo_aliases WHERE alias=LOWER($1) AND added_by=$2", alias, ctx.author.id) if not data: return await ctx.send( f"The alias `{alias.lower()}` is already in use.", allowed_mentions=discord.AllowedMentions.none(), ) await db("DELETE FROM github_repo_aliases WHERE alias=$1", alias) return await self.repoalias_add(ctx, alias, repo) await ctx.send("Done.") @repoalias.command( name="remove", aliases=['delete', 'del', 'rem'], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="alias", description="The alias that you want to remove.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True) async def repoalias_remove(self, ctx: vbu.Context, alias: str): """ Removes a Github repo alias from the database. """ async with vbu.Database() as db: data = await db( "SELECT * FROM github_repo_aliases WHERE alias=LOWER($1) AND added_by=$2", alias, ctx.author.id, ) if not data: return await ctx.send( "You don't own that repo alias.", allowed_mentions=discord.AllowedMentions.none(), ) await db("DELETE FROM github_repo_aliases WHERE alias=LOWER($1)", alias) await ctx.send("Done.") @commands.group( aliases=['issues'], application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def issue(self, ctx: vbu.Context): """ The parent group for the git issue commands. """ if ctx.invoked_subcommand is None: return await ctx.send_help(ctx.command) # @issue.command(name='frommessage', context_command_type=vbu.ApplicationCommandType.MESSAGE, context_command_name="Create issue from VBU webhook") # @commands.bot_has_permissions(send_messages=True, embed_links=True) # async def issue_frommessage(self, ctx: vbu.Context, message: discord.Message): # """ # Create a Github issue from a VBU error webhook. # """ # # Run some checks # if message.author.discriminator != "0000": # return await ctx.send("That message wasn't sent by a webhook.") # author_split = message.author.name.split("-") # if author_split[-1].strip() != "Error": # return await ctx.send("That message wasn't sent by a VBU error webhook.") # # Build up our content # repo_str = "-".join(author_split[:-1]).strip() # repo = await GitRepo.convert(ctx, repo_str) # body_match = VBU_ERROR_WEBHOOK_PATTERN.search(message.content) # title = f"Issue encountered running `{body_match.group('command_invoke').strip()}` command" # async with self.bot.session.get(message.attachments[0].url) as r: # error_file = await r.text() # body = ( # f"The bot hit a `{body_match.group('error').strip()}` error while running the `{body_match.group('command_invoke')}` " # f"command.\n```python\n{error_file.strip()}\n```" # ) # return await ctx.invoke(self.issue_create, repo, title=title, body=body) @issue.command( name='create', aliases=['make'], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="repo", description="The repo that you want to create an issue on.", type=discord.ApplicationCommandOptionType.string, autocomplete=True, ), discord.ApplicationCommandOption( name="title", description="The title of the issue that you want to make.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="body", description="Any body text for the issue.", type=discord.ApplicationCommandOptionType.string, required=False, ), ], ), ) @commands.bot_has_permissions(send_messages=True, embed_links=True) async def issue_create(self, ctx: vbu.Context, repo: GitRepo, *, title: str, body: str = ""): """ Create a Github issue on a given repo. """ # Get the database because whatever why not async with vbu.Database() as db: user_rows = await db("SELECT * FROM user_settings WHERE user_id=$1", ctx.author.id) if not user_rows or not user_rows[0][f'{repo.host.lower()}_username']: return await ctx.send( ( f"You need to link your {repo.host} account to Discord to run this " f"command - see the website at `{ctx.clean_prefix}info`." ), ) # Work out what components we want to use embed = vbu.Embed(title=title, description=body, use_random_colour=True).set_footer(text=str(repo)) components = discord.ui.MessageComponents.boolean_buttons() components.components[0].components.append(discord.ui.Button(label="Set title", custom_id="TITLE")) components.components[0].components.append(discord.ui.Button(label="Set body", custom_id="BODY")) components.components[0].components.append(discord.ui.Button(label="Set repo", custom_id="REPO")) options = [ discord.ui.SelectOption(label=i.name, value=i.name, description=i.description) for i in await repo.get_labels(user_rows[0]) ] components.add_component(discord.ui.ActionRow( discord.ui.SelectMenu( custom_id="LABELS", min_values=0, max_values=len(options), options=options, ) )) labels = [] # Ask if we want to do this m = None while True: # See if we want to update the body embed = vbu.Embed( title=title, description=body or "...", use_random_colour=True, ).set_footer( text=str(repo), ).add_field( "Labels", ", ".join([f"`{i}`" for i in labels]) or "...", ) if m is None: m = await ctx.send("Are you sure you want to create this issue?", embed=embed, components=components) else: await m.edit(embed=embed, components=components.enable_components()) try: payload = await self.bot.wait_for("component_interaction", check=vbu.component_check(ctx.author, m), timeout=120) except asyncio.TimeoutError: return await ctx.send("Timed out asking for issue create confirmation.") # Disable components if payload.component.custom_id not in ["LABELS"]: await payload.response.edit_message(components=components.disable_components()) # Get the body if payload.component.custom_id == "BODY": # Wait for their body message n = await payload.followup.send("What body content do you want to be added to your issue?") try: check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id body_message = await self.bot.wait_for("message", check=check, timeout=60 * 5) except asyncio.TimeoutError: return await payload.followup.send("Timed out asking for issue body text.") # Grab the attachments attachment_urls = [] for i in body_message.attachments: try: async with self.bot.session.get(i.url) as r: data = await r.read() file = discord.File(io.BytesIO(data), filename=i.filename) cache_message = await ctx.author.send(file=file) attachment_urls.append((file.filename, cache_message.attachments[0].url)) except discord.HTTPException: break # Delete their body message and our asking message try: await n.delete() await body_message.delete() except discord.HTTPException: pass # Fix up the body body = body.strip() + "\n\n" + body_message.content + "\n\n" for name, url in attachment_urls: body += f"![{name}]({url})\n" # Get the title elif payload.component.custom_id == "TITLE": # Wait for their body message n = await payload.followup.send("What do you want to set the issue title to?") try: check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id title_message = await self.bot.wait_for("message", check=check, timeout=60 * 5) except asyncio.TimeoutError: return await payload.followup.send("Timed out asking for issue title text.") # Delete their body message and our asking message try: await n.delete() await title_message.delete() except discord.HTTPException: pass title = title_message.content # Get the repo elif payload.component.custom_id == "REPO": # Wait for their body message n = await payload.followup.send("What do you want to set the repo to?") try: check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id repo_message = await self.bot.wait_for("message", check=check, timeout=60 * 5) except asyncio.TimeoutError: return await payload.followup.send("Timed out asking for issue title text.") # Delete their body message and our asking message try: await n.delete() await repo_message.delete() except discord.HTTPException: pass # Edit the message try: repo = await GitRepo.convert(ctx, repo_message.content) except Exception: await ctx.send(f"That repo isn't valid, {ctx.author.mention}", delete_after=3) # Get the labels elif payload.component.custom_id == "LABELS": await payload.response.defer_update() labels = payload.values # Check for exiting elif payload.component.custom_id == "NO": return await payload.followup.send("Alright, cancelling issue add.") elif payload.component.custom_id == "YES": break # Work out our args headers = {} if repo.host == "Github": json = {'title': title, 'body': body.strip(), 'labels': labels} headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f"token {user_rows[0]['github_access_token']}"} elif repo.host == "Gitlab": json = {'title': title, 'description': body.strip(), 'labels': ",".join(labels)} headers = {'Authorization': f"Bearer {user_rows[0]['gitlab_bearer_token']}"} else: raise Exception("Invalid host") headers.update({'User-Agent': self.bot.user_agent}) # Make the post request async with self.bot.session.post(repo.issue_api_url, json=json, headers=headers) as r: data = await r.json() self.logger.info(f"Received data from git {r.url!s} - {data!s}") if not r.ok: return await ctx.send(f"I was unable to create an issue on that repository - `{data}`.",) await ctx.send(f"Your issue has been created - <{data.get('html_url') or data.get('web_url')}>.",) await self.increase_repo_usage_counter(ctx.author, repo) @issue_create.autocomplete async def issue_create_autocomplete(self, ctx: commands.SlashContext, interaction: discord.Interaction): """ Send the user's most frequently used repos. """ if not interaction.user: return await interaction.response.send_autocomplete(None) async with vbu.Database() as db: rows = await db( """SELECT * FROM github_repo_uses WHERE user_id=$1 ORDER BY uses DESC""", interaction.user.id, ) responses = [ discord.ApplicationCommandOptionChoice(name=(repo := str(GitRepo(r['host'], r['owner'], r['repo']))), value=repo) for r in rows ]
class RolePicker(vbu.Cog[vbu.Bot]): @commands.group( application_command_meta=commands.ApplicationCommandMeta( guild_only=True, permissions=discord.Permissions(manage_roles=True, manage_guild=True), ), ) @commands.is_slash_command() async def rolepicker(self, _: vbu.SlashContext): pass @rolepicker.command( name="create", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="name", type=discord.ApplicationCommandOptionType.string, description="The name of your role picker. Only you can see this.", ), discord.ApplicationCommandOption( name="text", type=discord.ApplicationCommandOptionType.string, description="The text you want to be displayed above the dropdown.", ), discord.ApplicationCommandOption( name="channel", type=discord.ApplicationCommandOptionType.channel, description="The place you want to create the role picker.", channel_types=[discord.ChannelType.text], required=False, ), discord.ApplicationCommandOption( name="min_roles", type=discord.ApplicationCommandOptionType.integer, description="The minimum number of roles that the user can have.", min_value=0, required=False, ), discord.ApplicationCommandOption( name="max_roles", type=discord.ApplicationCommandOptionType.integer, description="The maximum number of roles that the user can have.", max_value=25, required=False, ), ], guild_only=True, permissions=discord.Permissions(manage_roles=True, manage_guild=True), ), ) @commands.is_slash_command() async def rolepicker_create( self, ctx: vbu.SlashContext, name: str, text: str, channel: Optional[discord.TextChannel] = None, min_roles: Optional[int] = None, max_roles: Optional[int] = None): """ Create a new role picker message. """ # Make sure the things they gave are valid if min_roles and max_roles and max_roles < min_roles: return await ctx.interaction.response.send_message( "The maximum cannot be a higher number than the minimum.", ephemeral=True, ) # Only work if they can send messages in that channel channel = channel or ctx.channel # type: ignore - will be a TextChannel, I guess assert channel author: discord.Member = ctx.author # type: ignore - will be a member assert author if not channel.permissions_for(author).send_messages: return await ctx.interaction.response.send_message( "You cannot send messages into that channel.", ephemeral=True, ) # Send message await ctx.interaction.response.defer(ephemeral=True) component_id = str(uuid4()) message = await channel.send( text, components=discord.ui.MessageComponents( discord.ui.ActionRow( discord.ui.SelectMenu( custom_id=f"ROLEPICKER {component_id}", options=[ discord.ui.SelectOption( label="No roles have been added :(", value="NULL", ), ], ), ), ), ) # Send and store async with vbu.Database() as db: await db.call( """ INSERT INTO role_pickers ( guild_id, name, message_id, channel_id, component_id, min_roles, max_roles ) VALUES ( $1, -- guild_id $2, -- name $3, -- message_id $4, -- channel_id $5, -- component_id $6, -- min_roles $7 -- max_role ) """, ctx.guild.id, name, message.id, message.channel.id, component_id, min_roles, max_roles, ) # And tell them about it await ctx.interaction.followup.send( "Created role picker!~", ephemeral=True, ) @rolepicker.command( name="delete", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="name", type=discord.ApplicationCommandOptionType.string, description="The name of your role picker.", autocomplete=True, ), ], guild_only=True, permissions=discord.Permissions(manage_roles=True, manage_guild=True), ), ) @commands.is_slash_command() async def rolepicker_delete( self, ctx: vbu.SlashContext, name: str): """ Remove a role picker message. """ # Defer so we can database call await ctx.interaction.response.defer(ephemeral=True) # Delete stored info async with vbu.Database() as db: role_picker_rows = await db.call( """ DELETE FROM role_pickers WHERE guild_id = $1 AND name = $2 RETURNING * """, ctx.guild.id, name, ) # See if we can delete the message as well row = role_picker_rows[0] messageable = self.bot.get_partial_messageable( row['channel_id'], type=discord.ChannelType.text, ) message = messageable.get_partial_message(row['message_id']) try: await message.delete() except: pass # And tell them about it await ctx.interaction.followup.send( "Deleted role picker.", ephemeral=True, ) @rolepicker.command( name="add", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="name", type=discord.ApplicationCommandOptionType.string, description="The name of the role picker that you want to modify.", autocomplete=True, ), discord.ApplicationCommandOption( name="role", type=discord.ApplicationCommandOptionType.role, description="The role you want to add to the role picker.", ), ], guild_only=True, permissions=discord.Permissions(manage_roles=True, manage_guild=True), ), ) @commands.is_slash_command() async def rolepicker_add( self, ctx: vbu.SlashContext, name: str, role: discord.Role): """ Add a new role to a role picker. """ # Defer so we can fetch await ctx.interaction.response.defer(ephemeral=True) # Fetch some values from the API author: discord.Member author = await ctx.guild.fetch_member(ctx.author.id) # type: ignore - author will definitely exist guild: discord.Guild = ctx.guild guild_roles = guild.roles # Make sure the role they gave is lower than their top author_top_role = [i for i in guild_roles if i.id == author.roles[-1].id][0] role = [i for i in guild_roles if i.id == role.id][0] if author_top_role < role: return await ctx.interaction.followup.send( "Your top role is below the one you're trying to manage.", ephemeral=True, ) # Add that role to the database async with vbu.Database() as db: await db.call( """ INSERT INTO role_picker_role ( guild_id, name, role_id ) VALUES ( $1, -- guild_id $2, -- name $3 -- role_id ) ON CONFLICT (guild_id, name, role_id) DO NOTHING """, guild.id, name, role.id, ) # Tell the user it's done await ctx.interaction.followup.send( "Added role to role picker!~", ephemeral=True, ) self.bot.dispatch("role_picker_update", guild, name) @rolepicker.command( name="remove", application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="name", type=discord.ApplicationCommandOptionType.string, description="The name of the role picker that you want to modify.", autocomplete=True, ), discord.ApplicationCommandOption( name="role", type=discord.ApplicationCommandOptionType.string, description="The role you want to remove from the role picker.", ), ], guild_only=True, permissions=discord.Permissions(manage_roles=True, manage_guild=True), ), ) @commands.is_slash_command() async def rolepicker_remove( self, ctx: vbu.SlashContext, name: str, role: discord.Role): """ Remove a role from one of your role pickers. """ # Defer so we can fetch await ctx.interaction.response.defer(ephemeral=True) # Remove that role from the database async with vbu.Database() as db: removed_role = await db.call( """ DELETE FROM role_picker_role WHERE guild_id = $1 AND name = $2 AND role_id = $3 RETURNING * """, ctx.guild.id, name, role.id, ) # See if anything was removed if removed_role: await ctx.interaction.followup.send( "Removed role from role picker.", ephemeral=True, ) else: await ctx.interaction.followup.send( "That role wasn't in the picker.", ephemeral=True, ) return self.bot.dispatch("role_picker_update", ctx.guild, name) @vbu.Cog.listener() async def on_role_picker_update( self, guild: Union[discord.abc.Snowflake, discord.Guild], role_picker_name_or_component_id: str): """ Update one of the published role pickers. """ # Get the current data async with vbu.Database() as db: role_rows = await db.call( """ SELECT role_pickers.message_id, role_pickers.channel_id, role_pickers.component_id, role_pickers.min_roles, role_pickers.max_roles, role_picker_role.role_id FROM role_picker_role LEFT JOIN role_pickers ON role_picker_role.guild_id = role_pickers.guild_id AND role_picker_role.name = role_pickers.name WHERE role_picker_role.guild_id = $1 AND ( role_pickers.name = $2 OR role_pickers.component_id = $2 ) """, guild.id, role_picker_name_or_component_id, ) if not role_rows: role_rows = await db.call( """ SELECT message_id, channel_id, component_id, min_roles, max_roles FROM role_pickers WHERE guild_id = $1 AND ( name = $2 OR component_id = $2 ) """, guild.id, role_picker_name_or_component_id, ) # Get a partial message we can edit messageable = self.bot.get_partial_messageable( role_rows[0]['channel_id'], type=discord.ChannelType.text, ) message = messageable.get_partial_message(role_rows[0]['message_id']) # Get the roles guild_roles: List[discord.Role] if isinstance(guild, discord.Guild): guild_roles = await guild.fetch_roles() else: guild = await self.bot.fetch_guild(guild.id) guild_roles = guild.roles # Create the menu options menu_options: List[discord.ui.SelectOption] = [ discord.ui.SelectOption( label="No roles have been added :(", value="NULL", ), ] if "role_id" in role_rows[0]: menu_options = [ discord.ui.SelectOption( label=role.name, value=i['role_id'], ) for i in role_rows if (role := discord.utils.get(guild_roles, id=i['role_id'])) ] # Edit the message row = role_rows[0] await message.edit( components=discord.ui.MessageComponents( discord.ui.ActionRow( discord.ui.SelectMenu( custom_id=f"ROLEPICKER {row['component_id']}", options=menu_options[:25], min_values=row['min_roles'], max_values=row['max_roles'], ), ), ), ) @vbu.Cog.listener() async def on_component_interaction( self, interaction: discord.Interaction[str]): """ Listen for a rolepicker component being clicked and manage that for the user. """ # Make sure it's a rolepicker component if not interaction.custom_id.startswith("ROLEPICKER"): return component_id = interaction.custom_id.split(" ")[1] # See what they selected picked_role_ids = [int(i) for i in interaction.values if i != "NULL"] # type: ignore - interaction values won't be none here if not picked_role_ids: return await interaction.response.defer_update() await interaction.response.defer(ephemeral=True) guild_id: int = interaction.guild_id # type: ignore - this will be run in a guild guild: discord.Guild = interaction.guild or await self.bot.fetch_guild(guild_id) picked_roles = [i for i in guild.roles if i.id in picked_role_ids] # See if they have any of those roles currently user: discord.Member = interaction.user # type: ignore - user is definitely a member object user_roles: List[discord.Role] = user.roles roles_they_have_currently = [i for i in user_roles if i in picked_roles] # See if they have ALL of the roles they picked if len(roles_they_have_currently) == len(picked_roles): try: await user.remove_roles(*picked_roles, reason="Role picker") # except discord.NotFound: # await interaction.followup.send( # "I can't find one of the roles you picked in the server any more.", # ephemeral=True, # ) # self.bot.dispatch("role_picker_update", guild, component_id) # return except discord.Forbidden: await interaction.followup.send( "I'm unable to remove that role from you.", ephemeral=True, ) return await interaction.followup.send( vbu.format("Removed {0} {0:plural,role,roles} from you.", len(picked_roles)), ephemeral=True, ) return # If not then we'll just add them to the user try: await user.add_roles(*picked_roles, reason="Role picker") except discord.NotFound: await interaction.followup.send( "I can't find one of the roles you picked in the server any more.", ephemeral=True, ) self.bot.dispatch("role_picker_update", guild, component_id) return except discord.Forbidden: await interaction.followup.send( "I'm unable to add that role to you.", ephemeral=True, ) return await interaction.followup.send( vbu.format("Added {0} {0:plural,role,roles} to you.", len(picked_roles)), ephemeral=True, ) return @rolepicker_add.autocomplete @rolepicker_remove.autocomplete @rolepicker_delete.autocomplete async def rolepicker_name_autocomplete( self, ctx: vbu.SlashContext, interaction: discord.Interaction): """ Handle autocompletes for rolepicker names. """ async with vbu.Database() as db: role_picker_rows = await db.call( """ SELECT name FROM role_pickers WHERE guild_id = $1 AND name LIKE '%' || $2 || '%' """, interaction.guild_id, interaction.options[0].options[0].value, ) return await interaction.response.send_autocomplete([ discord.ApplicationCommandOptionChoice(name=i['name'], value=i['name']) for i in role_picker_rows ])
class SteamCommand(vbu.Cog): ALL_GAMES_URL = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" GAME_DATA_URL = "https://store.steampowered.com/api/appdetails" GAME_URL_REGEX = re.compile(r"https:\/\/store\.steampowered\.com\/app\/(\d+)") def __init__(self, bot: vbu.Bot): super().__init__(bot) self.game_cache: dict = {} self.sent_message_cache = {} # MessageID: {embed: Embed, index: ScreenshotIndex, screenshots: List[str]} async def load_game_cache(self): """ Loads the games from Steam into cache. """ params = { "key": self.bot.config['api_keys']['steam'] } headers = { "User-Agent": self.bot.user_agent } async with self.bot.session.get(self.ALL_GAMES_URL, params=params, headers=headers) as r: data = await r.json() apps = data['applist']['apps'] self.game_cache = apps def get_valid_name(self, name): return ''.join(i for i in name if i.isdigit() or i.isalpha() or i.isspace()) @commands.command( aliases=['steamsearch'], application_command_meta=commands.ApplicationCommandMeta( options=[ discord.ApplicationCommandOption( name="app_name", description="The name of the game that you want to search for.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.defer() @vbu.checks.is_config_set('api_keys', 'steam') async def steam(self, ctx: vbu.Context, *, app_name: str): """ Search Steam for an item. """ # Load cache if not self.game_cache: await self.load_game_cache() # Get app app_object = None appid = None await ctx.trigger_typing() # By url match = self.GAME_URL_REGEX.search(app_name) if match is not None: app_name = match.group(1) # By app id if app_name.isdigit(): appid = int(app_name) try: app_object = [i for i in self.game_cache if i['appid'] == int(app_name)][0] except IndexError: pass # By app name if app_object is None and appid is None: app_name = self.get_valid_name(app_name) valid_items = [i for i in self.game_cache if app_name.lower() in self.get_valid_name(i['name']).lower()] full_title_match = [i for i in valid_items if app_name.lower() == self.get_valid_name(i['name']).lower()] if full_title_match: valid_items = [full_title_match[0]] if len(valid_items) > 1: output_items = valid_items[:10] output_ids = [f"`{i['appid']}` - {i['name']}" for i in output_items] return await ctx.send("There are multiple results with that name:\n" + "\n".join(output_ids)) # TODO elif len(valid_items) == 0: return await ctx.send("There are no results with that name.") app_object = valid_items[0] appid = app_object['appid'] # Get info params = { "appids": appid } headers = { "User-Agent": self.bot.user_agent } async with self.bot.session.get(self.GAME_DATA_URL, params=params, headers=headers) as r: game_data = await r.json() if game_data[str(appid)]['success'] is False: return await ctx.send(f"I couldn't find an application with ID `{appid}`.") game_object = game_data[str(appid)]['data'] # See if it's NSFW if int(game_object.get('required_age', '0')) >= 18 and ctx.channel.nsfw is False: return await ctx.send("That game is marked as an 18+, so can't be sent in a non-NSFW channel.") # Embed it babey with vbu.Embed(use_random_colour=True) as embed: embed.title = game_object['name'] embed.set_footer(text=f"AppID: {appid}") embed.description = game_object['short_description'] embed.add_field("Developer", ', '.join(game_object.get('developers', list())) or 'None', inline=True) embed.add_field("Publisher", ', '.join(game_object.get('publishers', list())) or 'None', inline=True) embed.add_field("Genre", ', '.join(i['description'] for i in game_object['genres']) or 'None', inline=True) if game_object.get('price_overview') is not None: initial_price = game_object['price_overview']['initial_formatted'] final_price = game_object['price_overview']['final_formatted'] embed.add_field("Price", f"~~{initial_price}~~ {final_price}" if initial_price else final_price, inline=True) embed.add_field("Link", f"Open with Steam - steam://store/{appid}\nOpen in browser - [Link](https://store.steampowered.com/app/{appid}/)", inline=False) screenshots = [i['path_full'] for i in game_object['screenshots']] embed.set_image(url=screenshots[0]) # Send m = await ctx.send(embed=embed)
class NicknameHandler(vbu.Cog): LETTER_REPLACEMENT_FILE_PATH = "config/letter_replacements.json" ASCII_CHARACTERS = string.ascii_letters + string.digits + string.punctuation def __init__(self, bot: vbu.Bot): super().__init__(bot) self.animal_names = None self.letter_replacements = None self.consecutive_character_regex = None async def get_animal_names(self): """ Grabs all names from the Github page. """ if self.animal_names: return self.animal_names headers = {"User-Agent": self.bot.user_agent} async with self.bot.session.get(ANIMAL_NAMES, headers=headers) as r: text = await r.text() self.animal_names = text.strip().split('\n') return await self.get_animal_names() def get_letter_replacements(self): """ Grabs all names from the Github page. """ if self.letter_replacements: return self.letter_replacements with open(self.LETTER_REPLACEMENT_FILE_PATH) as a: self.letter_replacements = json.load(a) for i in string.ascii_letters + string.punctuation + string.digits + string.whitespace: self.letter_replacements[i] = i return self.get_letter_replacements() @vbu.Cog.listener() async def on_member_join(self, member: discord.Member): """ Pings a member nickname update on member join. """ # Check if they are a bot if member.bot: return # See if they have a permanent nickname set async with vbu.Database() as db: data = await db( """SELECT nickname FROM permanent_nicknames WHERE guild_id=$1 AND user_id=$2""", member.guild.id, member.id) if data: try: await member.edit(nick=data[0]["nickname"], reason="Changed by Apple.Py automagically") self.logger.info( f"Set permanent nickname of {member.id} in {member.guild.id} from member join" ) except discord.Forbidden as e: self.logger.error( f"Couldn't set permanent nickname of {member.id} in {member.guild.id} - {e}" ) return # See if we want to fun their name if self.bot.guild_settings[ member.guild.id]['automatic_nickname_update']: self.logger.info( f"Pinging nickname update for member join (G{member.guild.id}/U{member.id})" ) await self.fix_user_nickname(member) @vbu.Cog.listener() async def on_member_update(self, before: discord.Member, member: discord.Member): """ Pings a member nickname update on nickname update. """ # Only ping if they've change nicknames/usernames if before.display_name == member.display_name: return # Only ping for non-moderators if member.guild_permissions.manage_nicknames: return # Only ping for non-bots if member.bot: return # See if they have a permanent nickname async with vbu.Database() as db: data = await db( """SELECT nickname FROM permanent_nicknames WHERE guild_id=$1 AND user_id=$2""", member.guild.id, member.id) if data: new_nickname = data[0]["nickname"] if member.nick == new_nickname: return try: await member.edit(nick=new_nickname, reason="Changed by Apple.Py automagically") self.logger.info( f"Set permanent nickname of {member.id} in {member.guild.id} to '{new_nickname}' from member update" ) except discord.Forbidden as e: self.logger.error( f"Couldn't set permanent nickname of {member.id} in {member.guild.id} - {e}" ) return # See if they're nickname banned if self.bot.guild_settings[ member.guild.id]['nickname_banned_role_id'] in member._roles: # See if their name was changed by an admin try: async for entry in member.guild.audit_logs( limit=1, action=discord.AuditLogAction.member_update): if entry.target.id == member.id: if entry.user.id != member.id: self.logger.info( f"Not pinging nickname update for a name changed by moderator (G{member.guild.id}/U{member.id})" ) return break except discord.Forbidden: return # Change nickname back try: await member.edit( nick=before.nick or before.name, reason="Changed by Apple.Py due to nickname ban role") self.logger.info( f"User {member.id} on guild {member.guild.id} changed nickname - changing back due to nickname ban role" ) except discord.Forbidden as e: self.logger.error( f"Can't change user {member.id}'s nickname on guild {member.guild.id} - {e}" ) return # See if we want to update their nickname if self.bot.guild_settings[ member.guild.id]['automatic_nickname_update']: # See if their name was changed by an admin try: async for entry in member.guild.audit_logs( limit=1, action=discord.AuditLogAction.member_update): if entry.target.id == member.id: if entry.user.id != member.id: self.logger.info( f"Not pinging nickname update for a name changed by moderator (G{member.guild.id}/U{member.id})" ) return break except discord.Forbidden: pass # Fix their name self.logger.info( f"Pinging nickname update for member update (G{member.guild.id}/U{member.id})" ) await self.fix_user_nickname(member) async def fix_user_nickname(self, user: discord.Member, *, force_to_animal: bool = False) -> str: """ Fix the nickname of a user. """ current_name = user.nick or user.name new_name = current_name # See if we should even bother trying to translate it if force_to_animal is False: # See if every other character is a space if new_name[1::2].strip() == "": new_name = new_name[::2] # Read the letter replacements file replacements = self.get_letter_replacements() # Make a translator translator = str.maketrans(replacements) # Try and fix their name new_name_with_zalgo = new_name.translate(translator) new_name = ''.join( [i for i in new_name_with_zalgo if i not in ZALGO_CHARACTERS]) # Remove obnoxious exclamation marks to boost to top of member list if current_name.startswith("! "): new_name = new_name.lstrip("! ") # See if they have enough valid characters if not self.consecutive_character_regex: chars = [ i if i not in string.punctuation else f"\\{i}" for i in self.ASCII_CHARACTERS ] self.consecutive_character_regex = re.compile( f"[{''.join(chars)}]{{3,}}") if force_to_animal or self.consecutive_character_regex.search( new_name) is None: new_name = random.choice(await self.get_animal_names()) # See if it needs editing if current_name == new_name: self.logger.info( f"Not updating the nickname '{new_name}' (G{user.guild.id}/U{user.id})" ) return new_name # Change their name self.logger.info( f"Updating nickname '{current_name}' to '{new_name}' (G{user.guild.id}/U{user.id})" ) await user.edit(nick=new_name, reason="Changed by Apple.Py automagically") return new_name @vbu.group(aliases=['fun'], invoke_without_command=True) @commands.has_permissions(manage_nicknames=True) @commands.bot_has_permissions(manage_nicknames=True) @commands.guild_only() async def fixunzalgoname(self, ctx: vbu.Context, user: discord.Member, force_to_animal: bool = False): """ Fixes a user's nickname to remove dumbass characters. """ if ctx.invoked_subcommand is not None: return assert ctx.guild user = await ctx.guild.fetch_member(user.id) current_name = user.nick or user.name new_name = await self.fix_user_nickname( user, force_to_animal=force_to_animal) return await ctx.send( f"Changed their name from `{current_name}` to `{new_name}`.") @fixunzalgoname.command( name="user", application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="user", description="The user whose username you want to fix.", type=discord.ApplicationCommandOptionType.user, ), ], ), ) @commands.has_permissions(manage_nicknames=True) @commands.bot_has_permissions(manage_nicknames=True) @commands.guild_only() async def fixunzalgoname_user(self, ctx: vbu.Context, user: discord.Member, force_to_animal: bool = False): """ Fixes a user's nickname to remove dumbass characters. """ await self.fixunzalgoname(ctx, user, force_to_animal) @fixunzalgoname.command(name='text') @commands.bot_has_permissions(send_messages=True) async def fixunzalgoname_text(self, ctx: vbu.Context, *, text: str): """ Fixes a user's nickname to remove dumbass characters. """ # Read the letter replacements file replacements = self.get_letter_replacements() # Make a translator translator = str.maketrans(replacements) # Try and fix their name new_name_with_zalgo = text.translate(translator) new_name = ''.join( [i for i in new_name_with_zalgo if i not in ZALGO_CHARACTERS]) # See if they have enough valid characters if not self.consecutive_character_regex: chars = [ i if i not in string.punctuation else f"\\{i}" for i in self.ASCII_CHARACTERS ] self.consecutive_character_regex = re.compile( f"[{''.join(chars)}]{{3,}}") if self.consecutive_character_regex.search(new_name) is None: new_name = random.choice(await self.get_animal_names()) return await ctx.send( f"I would change that from `{text}` to `{new_name}`.") @commands.command() @vbu.checks.is_bot_support() @commands.bot_has_permissions(send_messages=True) async def addfixablename(self, ctx: vbu.Context, user: discord.Member, *, fixed_name: str): """ Adds a given user's name to the fixable letters. """ await ctx.invoke(self.bot.get_command("addfixableletters"), user.display_name, fixed_name) await ctx.invoke(self.bot.get_command("fixunzalgoname"), user) @commands.command(ignore_extra=False) @vbu.checks.is_bot_support() @commands.bot_has_permissions(send_messages=True) async def addfixableletters(self, ctx: vbu.Context, phrase1: str, phrase2: str): """ Adds fixable letters to the replacement list. """ if len(phrase1) != len(phrase2): return await ctx.send( "The lengths of the two provided phrases don't match.") try: replacements = self.get_letter_replacements() except Exception as e: return await ctx.send( f"Could not open letter replacement file - `{e}`.") for i, o in zip(phrase1, phrase2): replacements[i] = o try: with open(self.LETTER_REPLACEMENT_FILE_PATH, "w") as a: json.dump(replacements, a) except Exception as e: return await ctx.send( f"Could not open letter replacement file to write to it - `{e}`." ) self.letter_replacements = None return await ctx.send("Written to file successfully.")
class ReminderCommands(vbu.Cog): def __init__(self, bot): super().__init__(bot) if bot.database.enabled: self.reminder_finish_handler.start() def cog_unload(self): self.reminder_finish_handler.stop() @commands.group( aliases=["reminders"], invoke_without_command=True, application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True) async def reminder(self, ctx: vbu.Context): """ The parent group for the reminder commands. """ if ctx.invoked_subcommand is not None: return return await ctx.send_help(ctx.command) @reminder.command( name="list", application_command_meta=commands.ApplicationCommandMeta(), ) @commands.bot_has_permissions(send_messages=True) async def reminder_list(self, ctx: vbu.Context): """ Shows you your reminders. """ # Get the guild ID try: guild_id = ctx.guild.id except AttributeError: guild_id = 0 # Grab their remidners async with vbu.Database() as db: rows = await db( "SELECT * FROM reminders WHERE user_id=$1 and guild_id=$2", ctx.author.id, guild_id) # Format an output string reminders = "" for reminder in rows: expiry = discord.utils.format_dt(reminder['timestamp']) reminders += f"\n`{reminder['reminder_id']}` - {reminder['message'][:70]} ({expiry})" message = f"Your reminders: {reminders}" # Send to the user await ctx.send(message or "You have no reminders.", allowed_mentions=discord.AllowedMentions.none()) @reminder.command( name="set", aliases=['create'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="time", description= "How far into the future you want to set the reminder.", type=discord.ApplicationCommandOptionType.string, ), discord.ApplicationCommandOption( name="message", description="The message that you want to set.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True) async def reminder_set(self, ctx: vbu.Context, time: vbu.TimeValue, *, message: str): """ Adds a reminder to your account. """ # Grab the guild ID try: guild_id = ctx.guild.id except AttributeError: guild_id = 0 # Get untaken id db = await vbu.Database.get_connection() while True: reminder_id = create_id() data = await db("SELECT * FROM reminders WHERE reminder_id=$1", reminder_id) if not data: break # Let them know its been set m = await ctx.send( f"Reminder set for {discord.utils.format_dt(discord.utils.utcnow() + time.delta)}." ) # Chuck the info in the database await db( """INSERT INTO reminders (reminder_id, guild_id, channel_id, message_id, timestamp, user_id, message) VALUES ($1, $2, $3, $4, $5, $6, $7)""", reminder_id, guild_id, ctx.channel.id, m.id, (discord.utils.utcnow() + time.delta).replace(tzinfo=None), ctx.author.id, message, ) await db.disconnect() @reminder.command( name="delete", aliases=['remove'], application_command_meta=commands.ApplicationCommandMeta(options=[ discord.ApplicationCommandOption( name="reminder_id", description="The ID of the reminder that you want to delete.", type=discord.ApplicationCommandOptionType.string, ), ], ), ) @commands.bot_has_permissions(send_messages=True) async def reminder_delete(self, ctx: vbu.Context, reminder_id: str): """ Deletes a reminder from your account. """ # Grab the guild ID try: guild_id = ctx.guild.id except AttributeError: guild_id = 0 # Grab the reminder async with self.bot.database() as db: data = await db( "SELECT * FROM reminders WHERE reminder_id=$1 and guild_id=$2", reminder_id, guild_id) # Check if it exists if not data: return await ctx.send("That reminder doesn't exist.") # Delete it await db( "DELETE FROM reminders WHERE reminder_id=$1 and user_id=$2", reminder_id, ctx.author.id) # Send feedback saying it was deleted await ctx.send("Reminder deleted.") @tasks.loop(seconds=30) async def reminder_finish_handler(self): """ Handles reminders expiring. """ # Grab finished stuff from the database db = await vbu.Database.get_connection() rows = await db( "SELECT * FROM reminders WHERE timestamp < TIMEZONE('UTC', NOW())") if not rows: await db.disconnect() return # Go through all finished reminders expired_reminders = [] for reminder in rows: channel_id = reminder["channel_id"] user_id = reminder["user_id"] message_id = reminder["message_id"] message = reminder["message"] reminder_id = reminder["reminder_id"] try: channel = self.bot.get_channel( channel_id) or await self.bot.fetch_channel(channel_id) except discord.HTTPException: channel = None sendable = { "content": f"<@{user_id}> reminder `{reminder_id}` triggered - {message}", "allowed_mentions": discord.AllowedMentions(users=[discord.Object(user_id)]), } if message_id: sendable.update({ "reference": discord.MessageReference(message_id=message_id, channel_id=channel_id), "mention_author": True, }) try: assert channel is not None try: await channel.send(**sendable) except Exception: sendable.pop("reference") await channel.send(**sendable) except (AssertionError, discord.Forbidden): try: user = self.bot.get_user( user_id) or await self.bot.fetch_user(user_id) await user.send(**sendable) except discord.HTTPException: pass except AttributeError: pass expired_reminders.append(reminder_id) # Delete expired reminders await db("DELETE FROM reminders WHERE reminder_id=ANY($1::TEXT[])", expired_reminders) await db.disconnect()