async def typing_history(self, ctx: commands.Context, member: discord.Member = None): """See your typing test history""" if member is None: member = ctx.author data = await self.bot.db.execute( """ SELECT test_date, wpm, accuracy, word_count, test_language FROM typing_stats WHERE user_id = %s ORDER BY test_date DESC """, member.id, ) if not data: raise exceptions.CommandInfo(("You haven't" if member is ctx.author else f"**{member.name}** hasn't") + " taken any typing tests yet!", ) content = discord.Embed( title=f":stopwatch: {util.displayname(member)} Typing test history", color=int("dd2e44", 16), ) content.set_footer(text=f"Total {len(data)} typing tests taken") rows = [] for test_date, wpm, accuracy, word_count, test_language in data: rows.append( f"**{wpm}** WPM, **{int(accuracy)}%** ACC, " f"**{word_count}** words, *{test_language}* ({arrow.get(test_date).to('utc').humanize()})" ) await util.send_as_pages(ctx, content, rows)
async def send_hs(self, ctx: commands.Context, day): sunsign = await self.bot.db.execute( "SELECT sunsign FROM user_settings WHERE user_id = %s", ctx.author.id, one_value=True, ) if not sunsign or sunsign is None: raise exceptions.CommandInfo( "Please save your zodiac sign using `>horoscope set <sign>`\n" "Use `>horoscope list` if you don't know which one you are." ) params = {"sign": sunsign, "day": day} async with self.bot.session.post( "https://aztro.sameerkumar.website/", params=params ) as response: data = await response.json(loads=orjson.loads) sign = self.hs.get(sunsign) content = discord.Embed( color=int("9266cc", 16), title=f"{sign['emoji']} {sign['name']} - {data['current_date']}", description=data["description"], ) content.add_field(name="Mood", value=data["mood"], inline=True) content.add_field(name="Compatibility", value=data["compatibility"], inline=True) content.add_field(name="Color", value=data["color"], inline=True) content.add_field(name="Lucky number", value=data["lucky_number"], inline=True) content.add_field(name="Lucky time", value=data["lucky_time"], inline=True) content.add_field(name="Date range", value=data["date_range"], inline=True) await ctx.send(embed=content)
async def remindme(self, ctx: commands.Context, pre, *, arguments): """ Set a reminder Usage: >remindme in <some time> to <something> >remindme on <YYYY/MM/DD> [HH:mm:ss] to <something> """ try: reminder_time, content = arguments.split(" to ", 1) except ValueError: return await util.send_command_help(ctx) now = arrow.utcnow() if pre == "on": # user inputs date date = arrow.get(reminder_time) seconds = date.int_timestamp - now.int_timestamp elif pre == "in": # user inputs time delta seconds = util.timefromstring(reminder_time) date = now.shift(seconds=+seconds) else: return await ctx.send( f"Invalid operation `{pre}`\nUse `on` for date and `in` for time delta" ) if seconds < 1: raise exceptions.CommandInfo( "You must give a valid time at least 1 second in the future!") await self.bot.db.execute( """ INSERT INTO reminder (user_id, guild_id, created_on, reminder_date, content, original_message_url) VALUES(%s, %s, %s, %s, %s, %s) """, ctx.author.id, ctx.guild.id, now.datetime, date.datetime, content, ctx.message.jump_url, ) self.cache_needs_refreshing = True await ctx.send(embed=discord.Embed( color=int("ccd6dd", 16), description= (f":pencil: I'll message you on **{date.to('utc').format('DD/MM/YYYY HH:mm:ss')}" f" UTC** to remind you of:\n```{content}```"), ))
async def command_list(self, ctx: commands.Context): """List all commands on this server""" rows = [] for command in await self.custom_command_list(ctx.guild.id): rows.append(f"{ctx.prefix}{command}") if rows: content = discord.Embed(title=f"{ctx.guild.name} custom commands") await util.send_as_pages(ctx, content, rows) else: raise exceptions.CommandInfo( "No custom commands have been added on this server yet")
async def melon(self, ctx: commands.Context, timeframe): """Melon music charts""" if timeframe not in ["day", "month"]: if timeframe == "realtime": timeframe = "" elif timeframe == "rising": timeframe = "rise" else: raise exceptions.CommandInfo( "Available timeframes: `[ day | month | realtime | rising ]`" ) url = f"https://www.melon.com/chart/{timeframe}/index.htm" headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0", } async with self.bot.session.get(url, headers=headers) as response: soup = BeautifulSoup(await response.text(), "html.parser") song_titles = [ util.escape_md(x.find("span").find("a").text) for x in soup.find_all("div", {"class": "ellipsis rank01"}) ] artists = [ util.escape_md(x.find("a").text) for x in soup.find_all("div", {"class": "ellipsis rank02"}) ] # albums = [ # util.escape_md(x.find("a").text) # for x in soup.find_all("div", {"class": "ellipsis rank03"}) # ] image = soup.find("img", { "onerror": "WEBPOCIMG.defaultAlbumImg(this);" }).get("src") content = discord.Embed(color=discord.Color.from_rgb(0, 205, 60)) content.set_author( name=f"Melon top {len(song_titles)}" + ("" if timeframe == "" else f" - {timeframe.capitalize()}"), url=url, icon_url="https://i.imgur.com/hm9xzPz.png", ) content.set_thumbnail(url=image) content.timestamp = ctx.message.created_at rows = [] for i, (song, artist) in enumerate(zip(song_titles, artists), start=1): rows.append(f"`#{i:2}` **{artist}** — ***{song}***") await util.send_as_pages(ctx, content, rows)
async def ban(self, ctx: commands.Context, *discord_users): """Ban user(s)""" if not discord_users: return await util.send_command_help(ctx) if len(discord_users) > 4: raise exceptions.CommandInfo( f"It seems you are trying to ban a lot of users at once.\nPlease use `{ctx.prefix}massban ...` instead" ) for discord_user in discord_users: user = await util.get_member(ctx, discord_user) if user is None: try: user = await self.bot.fetch_user(int(discord_user)) except (ValueError, discord.NotFound): await ctx.send(embed=discord.Embed( description= f":warning: Invalid user or id `{discord_user}`", color=int("be1931", 16), )) continue if user.id == 133311691852218378: return await ctx.send("no.") # confirmation dialog for guild members if isinstance(user, discord.Member): await self.send_ban_confirmation(ctx, user) elif isinstance(user, discord.User): try: await ctx.guild.ban(user, delete_message_days=0) except discord.errors.Forbidden: await ctx.send(embed=discord.Embed( description= f":no_entry: It seems I don't have the permission to ban **{user}**", color=int("be1931", 16), )) else: await ctx.send(embed=discord.Embed( description=f":hammer: Banned `{user}`", color=int("f4900c", 16), )) else: await ctx.send(embed=discord.Embed( description= f":warning: Invalid user or id `{discord_user}`", color=int("be1931", 16), ))
async def horoscope_set(self, ctx: commands.Context, sign): """Save your zodiac sign""" sign = sign.lower() if self.hs.get(sign) is None: raise exceptions.CommandInfo( f"`{sign}` is not a valid zodiac! Use `>horoscope list` for a list of zodiacs." ) await ctx.bot.db.execute( """ INSERT INTO user_settings (user_id, sunsign) VALUES (%s, %s) ON DUPLICATE KEY UPDATE sunsign = VALUES(sunsign) """, ctx.author.id, sign, ) await ctx.send(f"Zodiac saved as **{sign.capitalize()}** {self.hs.get(sign)['emoji']}")
async def votechannel_list(self, ctx: commands.Context): """List all current voting channels on this server""" channels = await self.bot.db.execute( """ SELECT channel_id, voting_type FROM voting_channel WHERE guild_id = %s """, ctx.guild.id, ) if not channels: raise exceptions.CommandInfo( "There are no voting channels on this server yet!") rows = [] for channel_id, voting_type in channels: rows.append(f"<#{channel_id}> - `{voting_type}`") content = discord.Embed( title=f":1234: Voting channels in {ctx.guild.name}", color=int("3b88c3", 16)) await util.send_as_pages(ctx, content, rows)
async def notification_list(self, ctx: commands.Context): """List your current notifications""" words = await self.bot.db.execute( """ SELECT guild_id, keyword, times_triggered FROM notification WHERE user_id = %s ORDER BY keyword """, ctx.author.id, ) if not words: raise exceptions.CommandInfo( "You have not set any notifications yet!") content = discord.Embed( title=f":love_letter: You have {len(words)} notifications", color=int("dd2e44", 16), ) rows = [] for guild_id, keyword, times_triggered in sorted(words): guild = self.bot.get_guild(guild_id) if guild is None: guild = f"[Unknown server `{guild_id}`]" rows.append( f"**{guild}** : `{keyword}` - Triggered **{times_triggered}** times" ) try: await util.send_as_pages(ctx.author, content, rows, maxpages=1, maxrows=50) except discord.errors.Forbidden: raise exceptions.CommandWarning( "I was unable to send you a DM! Please change your settings.") if ctx.guild is not None: await util.send_success( ctx, f"Notification list sent to your DM {emojis.VIVISMIRK}")
async def leaderboard_fishy(self, ctx: commands.Context, scope=""): """Fishy leaderboard""" global_data = scope.lower() == "global" data = await self.bot.db.execute( "SELECT user_id, fishy_count FROM fishy ORDER BY fishy_count DESC") rows = [] medal_emoji = [":first_place:", ":second_place:", ":third_place:"] i = 1 for user_id, fishy_count in data: if global_data: user = self.bot.get_user(user_id) else: user = ctx.guild.get_member(user_id) if user is None or fishy_count == 0: continue if i <= len(medal_emoji): ranking = medal_emoji[i - 1] else: ranking = f"`#{i:2}`" rows.append( f"{ranking} **{util.displayname(user)}** — **{fishy_count}** fishy" ) i += 1 if not rows: raise exceptions.CommandInfo("Nobody has any fish yet!") content = discord.Embed( title= f":fish: {'Global' if global_data else ctx.guild.name} fishy leaderboard", color=int("55acee", 16), ) await util.send_as_pages(ctx, content, rows)
async def typing_stats(self, ctx: commands.Context, user: discord.Member = None): """See your typing statistics""" if user is None: user = ctx.author data = await self.bot.db.execute( """ SELECT COUNT(test_date), MAX(wpm), AVG(wpm), AVG(accuracy), race_count, win_count FROM typing_stats LEFT JOIN typing_race ON typing_stats.user_id = typing_race.user_id WHERE typing_stats.user_id = %s GROUP BY typing_stats.user_id """, user.id, one_row=True, ) if not data: raise exceptions.CommandInfo( ("You haven't" if user is ctx.author else f"**{user.name}** hasn't") + " taken any typing tests yet!", ) test_count, max_wpm, avg_wpm, avg_acc, race_count, win_count = data content = discord.Embed( title=f":bar_chart: Typing stats for {user.name}", color=int("3b94d9", 16)) content.description = ( f"Best WPM: **{max_wpm}**\n" f"Average WPM: **{int(avg_wpm)}**\n" f"Average Accuracy: **{avg_acc:.2f}%**\n" f"Tests taken: **{test_count}** of which **{race_count}** were races\n" f"Races won: **{win_count or 0}** " + (f"(**{(win_count/race_count)*100:.1f}%** win rate)" if race_count is not None else "")) await ctx.send(embed=content)
async def timeout(self, ctx: commands.Context, member: discord.Member, *, duration="1 hour"): """Timeout user. Pass 'remove' as the duration to remove""" if member.timeout is not None: seconds = member.timeout.timestamp() - arrow.now().int_timestamp if duration and duration.strip().lower() == "remove": await member.edit(timeout=None) return await util.send_success( ctx, f"Removed timeout from {member.mention}") else: raise exceptions.CommandInfo( f"{member.mention} is already timed out (**{util.stringfromtime(seconds)}** remaining)", ) seconds = util.timefromstring(duration) await member.edit(timeout=arrow.now().shift(seconds=+seconds).datetime) await util.send_success( ctx, f"Timed out {member.mention} for **{util.stringfromtime(seconds)}**" )
async def minecraft(self, ctx: commands.Context, address=None, port=None): """Get the status of a minecraft server""" if address == "set": if port is None: return await ctx.send( f"Save minecraft server address for this discord server:\n" f"`{ctx.prefix}minecraft set <address>` (port defaults to 25565)\n" f"`{ctx.prefix}minecraft set <address>:<port>`" ) address = port.split(":")[0] try: port = int(port.split(":")[1]) except IndexError: port = 25565 await self.bot.db.execute( """ INSERT INTO minecraft_server (guild_id, server_address, port) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE server_address = VALUES(server_address), port = VALUES(port) """, ctx.guild.id, address, port, ) return await util.send_success( ctx, f"Default Minecraft server of this discord saved as `{address}:{port}`", ) if address is None: data = await self.bot.db.execute( "SELECT server_address, port FROM minecraft_server WHERE guild_id = %s", ctx.guild.id, one_row=True, ) if not data: raise exceptions.CommandInfo( "No default Minecraft server saved for this discord server!" f"Use `{ctx.prefix}minecraft set` to save one or" f"`{ctx.prefix}minecraft <address> <port>` to see any server" ) address, port = data if port is None: port = 25565 server = await self.bot.loop.run_in_executor( None, lambda: minestat.MineStat(address, int(port)) ) content = discord.Embed(color=discord.Color.green()) if server.online: content.add_field(name="Server Address", value=f"`{server.address}`") content.add_field(name="Version", value=server.version) content.add_field( name="Players", value=f"{server.current_players}/{server.max_players}" ) content.add_field(name="Latency", value=f"{server.latency}ms") content.set_footer(text=f"Message of the day: {server.motd}") else: content.title = f"{address}:{port}" content.description = ":warning: **Server is offline**" content.set_thumbnail(url="https://i.imgur.com/P1IxD0Q.png") await ctx.send(embed=content)
async def mute(self, ctx: commands.Context, member: discord.Member, *, duration=None): """Mute user""" mute_role_id = await self.bot.db.execute( """ SELECT mute_role_id FROM guild_settings WHERE guild_id = %s """, ctx.guild.id, one_value=True, ) mute_role = ctx.guild.get_role(mute_role_id) if not mute_role: raise exceptions.CommandWarning( "Mute role for this server has been deleted or is not set, " f"please use `{ctx.prefix}muterole <role>` to set it.") if member.id == 133311691852218378: return await ctx.send("no.") seconds = None if duration is not None: seconds = util.timefromstring(duration) if seconds is None or seconds == 0: raise exceptions.CommandWarning( f'Invalid mute duration "{duration}"') if seconds < 60: raise exceptions.CommandInfo( "The minimum duration of a mute is **1 minute**") if seconds > 604800: raise exceptions.CommandInfo( "The maximum duration of a mute is **1 week**") try: await member.add_roles(mute_role) except discord.errors.Forbidden: raise exceptions.CommandError( f"It seems I don't have permission to mute {member.mention}") await util.send_success( ctx, f"Muted {member.mention}" + (f" for **{util.stringfromtime(seconds)}**" if seconds is not None else ""), ) if seconds is not None: unmute_on = arrow.now().shift(seconds=seconds).datetime else: unmute_on = None await self.bot.db.execute( """ INSERT INTO muted_user (guild_id, user_id, channel_id, unmute_on) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE unmute_on = VALUES(unmute_on) """, ctx.guild.id, member.id, ctx.channel.id, unmute_on, ) self.cache_needs_refreshing = True
async def commandstats_single(self, ctx: commands.Context, command_name): """Stats of a single command""" command = self.bot.get_command(command_name) if command is None: raise exceptions.CommandInfo(f"Command `{ctx.prefix}{command_name}` does not exist!") content = discord.Embed(title=f":bar_chart: `{ctx.prefix}{command.qualified_name}`") # set command name to be tuple of subcommands if this is a command group group = hasattr(command, "commands") if group: command_name = tuple( [f"{command.name} {x.name}" for x in command.commands] + [command_name] ) else: command_name = command.qualified_name (total_uses, most_used_by_user_id, most_used_by_user_amount,) = await self.bot.db.execute( f""" SELECT SUM(use_sum) as total, user_id, MAX(use_sum) FROM ( SELECT SUM(uses) as use_sum, user_id FROM command_usage WHERE command_type = 'internal' AND command_name {'IN %s' if group else '= %s'} GROUP BY user_id ) as subq """, command_name, one_row=True, ) most_used_by_guild_id, most_used_by_guild_amount = await self.bot.db.execute( f""" SELECT guild_id, MAX(use_sum) FROM ( SELECT guild_id, SUM(uses) as use_sum FROM command_usage WHERE command_type = 'internal' AND command_name {'IN %s' if group else '= %s'} GROUP BY guild_id ) as subq """, command_name, one_row=True, ) uses_in_this_server = ( await self.bot.db.execute( f""" SELECT SUM(uses) FROM command_usage WHERE command_type = 'internal' AND command_name {'IN %s' if group else '= %s'} AND guild_id = %s GROUP BY guild_id """, command_name, ctx.guild.id, one_value=True, ) or 0 ) # show the data in embed fields content.add_field(name="Uses", value=total_uses or 0) content.add_field(name="on this server", value=uses_in_this_server) content.add_field( name="Server most used in", value=f"{self.bot.get_guild(most_used_by_guild_id)} ({most_used_by_guild_amount or 0})", inline=False, ) content.add_field( name="Most total uses by", value=f"{self.bot.get_user(most_used_by_user_id)} ({most_used_by_user_amount or 0})", ) # additional data for command groups if group: content.description = "Command Group" subcommands_tuple = tuple([f"{command.name} {x.name}" for x in command.commands]) subcommand_usage = await self.bot.db.execute( """ SELECT command_name, SUM(uses) FROM command_usage WHERE command_type = 'internal' AND command_name IN %s GROUP BY command_name ORDER BY SUM(uses) DESC """, subcommands_tuple, ) content.add_field( name="Subcommand usage", value="\n".join(f"{s[0]} - **{s[1]}**" for s in subcommand_usage), inline=False, ) await ctx.send(embed=content)