async def typing_stats(self, ctx, user: discord.Member = None): """See your typing test 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.Info( ("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 typing_history(self, ctx, user: discord.Member = None): """See your typing test history.""" if user is None: user = 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 """, user.id, ) if not data: raise exceptions.Info( ctx, ("You haven't" if user is ctx.author else f"**{user.name}** hasn't") + " taken any typing tests yet!", ) content = discord.Embed( title=f":stopwatch: Typing history for {user.name}", 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 list(self, ctx): """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.Info("You have not set any notifications yet!") content = discord.Embed(title=":love_letter: Your notifications", color=int("dd2e44", 16)) rows = [] for guild_id, keyword, times_triggered in words: if guild_id == 0: guild = "globally" else: guild = f"in {self.bot.get_guild(guild_id)}" rows.append( f'`"{keyword}"` *{guild}* - triggered **{times_triggered}** times' ) try: await util.send_as_pages(ctx.author, content, rows) except discord.errors.Forbidden: raise exceptions.Warning( "I was unable to send you a DM. Please change your settings.") if ctx.guild is not None: await ctx.send( f"Notification list sent to your DM {emojis.VIVISMIRK}")
async def remindme(self, ctx, 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 ") except ValueError: return await util.send_command_help(ctx) now = arrow.now() if pre == "on": # user inputs date date = arrow.get(reminder_time) seconds = date.timestamp - now.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.Info( "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 melon(self, ctx, timeframe): """Melon music charts.""" if timeframe not in ["day", "month"]: if timeframe == "realtime": timeframe = "" elif timeframe == "rising": timeframe = "rise" else: raise exceptions.Info( "Available timeframes: `[ day | month | realtime | rising ]`" ) url = f"https://www.melon.com/chart/{timeframe}/index.htm" async with aiohttp.ClientSession() as session: 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 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 list(self, ctx): """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.Info( "No custom commands have been added on this server yet")
async def mediaonly_user(self, ctx, username, value: bool): """User level setting.""" user_id = await self.bot.db.execute( """ SELECT twitter_user.user_id FROM follow RIGHT JOIN twitter_user ON twitter_user.user_id = follow.twitter_user_id WHERE twitter_user.username = %s AND guild_id = %s""", username, ctx.guild.id, one_value=True, ) if not user_id: raise exceptions.Info(f'No channel on this server is following "@{username}"') await queries.set_config_user(self.bot.db, ctx.guild.id, user_id, "media_only", value) await ctx.send( f":white_check_mark: User setting **Media only** is now **{value}** for **@{username}**" )
async def votechannel_list(self, ctx): """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.Info("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 remove(self, ctx, channel: discord.TextChannel, *usernames): """Remove users from the follow list.""" if not usernames: raise exceptions.Info("You must give at least one twitter user to remove!") rows = [] current_users = await self.bot.db.execute( "SELECT twitter_user_id FROM follow WHERE channel_id = %s", channel.id ) successes = 0 for username in usernames: status = None try: user_id = (await self.api.get_user(username=username)).data.id except Exception as e: # user not found, maybe changed username # try finding username from cache user_id = await self.bot.db.execute( "SELECT user_id FROM twitter_user WHERE username = %s", username ) if user_id: user_id = user_id[0][0] else: status = f":x: Error {e.args[0][0]['code']}: {e.args[0][0]['message']}" if status is None: if (user_id,) not in current_users: status = ":x: User is not being followed on this channel" else: await self.unfollow(channel.id, user_id) status = ":white_check_mark: Success" successes += 1 rows.append(f"**@{username}** {status}") content = discord.Embed( title=f":notepad_spiral: Removed {successes}/{len(usernames)} users from {channel.name}", color=self.bot.twitter_blue, ) content.set_footer(text="Changes will take effect within a minute") pages = menus.Menu(source=menus.ListMenu(rows, embed=content), clear_reactions_after=True) await pages.start(ctx)
async def add(self, ctx, channel: discord.TextChannel, *usernames): """Add users to the follow list.""" if not usernames: raise exceptions.Info("You must give at least one twitter user to follow!") rows = [] time_now = arrow.now().datetime current_users = await self.bot.db.execute( "SELECT twitter_user_id FROM follow WHERE channel_id = %s", channel.id ) guild_follow_current, guild_follow_limit = await queries.get_follow_limit( self.bot.db, channel.guild.id ) successes = 0 for username in usernames: status = None try: user = (await self.api.get_user(username=username)).data except Exception as e: status = f":x: Error {e}" else: if (user.id,) in current_users: status = ":x: User already being followed on this channel" else: if guild_follow_current >= guild_follow_limit: status = f":lock: Guild follow count limit reached ({guild_follow_limit})" else: await self.follow(channel, user.id, user.username, time_now) status = ":white_check_mark: Success" successes += 1 guild_follow_current += 1 rows.append(f"**@{user.username}** {status}") content = discord.Embed( title=f":notepad_spiral: Added {successes}/{len(usernames)} users to {channel.name}", color=self.bot.twitter_blue, ) content.set_footer(text="Changes will take effect within a minute") pages = menus.Menu(source=menus.ListMenu(rows, embed=content), clear_reactions_after=True) await pages.start(ctx)
async def set(self, ctx, sign): """Save your zodiac sign.""" sign = sign.lower() if self.hs.get(sign) is None: raise exceptions.Info( 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 list(self, ctx): """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.Info("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.Warning( "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 send_hs(self, ctx, 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.Info( "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 aiohttp.ClientSession() as session: async with session.post("https://aztro.sameerkumar.website/", params=params) as response: data = await response.json() 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 leaderboard_fishy(self, ctx, 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.Info("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 mute(self, ctx, 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.Warning( "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.Warning(f'Invalid mute duration "{duration}"') if seconds < 60: raise exceptions.Info( "The minimum duration of a mute is **1 minute**") if seconds > 604800: raise exceptions.Info( "The maximum duration of a mute is **1 week**") try: await member.add_roles(mute_role) except discord.errors.Forbidden: raise exceptions.Error( 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, command_name): """Stats of a single command.""" command = self.bot.get_command(command_name) if command is None: raise exceptions.Info(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)
async def minecraft(self, ctx, 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.Info( "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)