import datetime import time from enum import Enum from random import randint, choice from typing import Final import aiohttp import discord from redbot.core import commands from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from redbot.core.utils.chat_formatting import ( bold, escape, italics, humanize_number, humanize_timedelta, ) _ = T_ = Translator("General", __file__) class RPS(Enum): rock = "\N{MOYAI}" paper = "\N{PAGE FACING UP}" scissors = "\N{BLACK SCISSORS}" class RPSParser: def __init__(self, argument): argument = argument.lower() if argument == "rock": self.choice = RPS.rock elif argument == "paper": self.choice = RPS.paper elif argument == "scissors": self.choice = RPS.scissors else: self.choice = None MAX_ROLL: Final[int] = 2 ** 64 - 1 @cog_i18n(_) class General(commands.Cog): """General commands.""" global _ _ = lambda s: s ball = [ _("As I see it, yes"), _("It is certain"), _("It is decidedly so"), _("Most likely"), _("Outlook good"), _("Signs point to yes"), _("Without a doubt"), _("Yes"), _("Yes – definitely"), _("You may rely on it"), _("Reply hazy, try again"), _("Ask again later"), _("Better not tell you now"), _("Cannot predict now"), _("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"), _("My sources say no"), _("Outlook not so good"), _("Very doubtful"), ] _ = T_ def __init__(self): super().__init__() self.stopwatches = {} async def red_delete_data_for_user(self, **kwargs): """ Nothing to delete """ return @commands.command() async def choose(self, ctx, *choices): """Choose between multiple options. To denote options which include whitespace, you should use double quotes. """ choices = [escape(c, mass_mentions=True) for c in choices] if len(choices) < 2: await ctx.send(_("Not enough options to pick from.")) else: await ctx.send(choice(choices)) @commands.command() async def roll(self, ctx, number: int = 100): """Roll a random number. The result will be between 1 and ``. `` defaults to 100. """ author = ctx.author if 1 < number <= MAX_ROLL: n = randint(1, number) await ctx.send( "{author.mention} :game_die: {n} :game_die:".format( author=author, n=humanize_number(n) ) ) elif number <= 1: await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author)) else: await ctx.send( _("{author.mention} Max allowed number is {maxamount}.").format( author=author, maxamount=humanize_number(MAX_ROLL) ) ) @commands.command() async def flip(self, ctx, user: discord.Member = None): """Flip a coin... or a user. Defaults to a coin. """ if user is not None: msg = "" if user.id == ctx.bot.user.id: user = ctx.author msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n") char = "abcdefghijklmnopqrstuvwxyz" tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz" table = str.maketrans(char, tran) name = user.display_name.translate(table) char = char.upper() tran = "∀qƆpƎℲפHIſʞ˥WNOԀQᴚS┴∩ΛMX⅄Z" table = str.maketrans(char, tran) name = name.translate(table) await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1]) else: await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])) @commands.command() async def rps(self, ctx, your_choice: RPSParser): """Play Rock Paper Scissors.""" author = ctx.author player_choice = your_choice.choice if not player_choice: return await ctx.send( _("This isn't a valid option. Try {r}, {p}, or {s}.").format( r="rock", p="paper", s="scissors" ) ) red_choice = choice((RPS.rock, RPS.paper, RPS.scissors)) cond = { (RPS.rock, RPS.paper): False, (RPS.rock, RPS.scissors): True, (RPS.paper, RPS.rock): True, (RPS.paper, RPS.scissors): False, (RPS.scissors, RPS.rock): False, (RPS.scissors, RPS.paper): True, } if red_choice == player_choice: outcome = None # Tie else: outcome = cond[(player_choice, red_choice)] if outcome is True: await ctx.send( _("{choice} You win {author.mention}!").format( choice=red_choice.value, author=author ) ) elif outcome is False: await ctx.send( _("{choice} You lose {author.mention}!").format( choice=red_choice.value, author=author ) ) else: await ctx.send( _("{choice} We're square {author.mention}!").format( choice=red_choice.value, author=author ) ) @commands.command(name="8", aliases=["8ball"]) async def _8ball(self, ctx, *, question: str): """Ask 8 ball a question. Question must end with a question mark. """ if question.endswith("?") and question != "?": await ctx.send("`" + T_(choice(self.ball)) + "`") else: await ctx.send(_("That doesn't look like a question.")) @commands.command(aliases=["sw"]) async def stopwatch(self, ctx): """Start or stop the stopwatch.""" author = ctx.author if author.id not in self.stopwatches: self.stopwatches[author.id] = int(time.perf_counter()) await ctx.send(author.mention + _(" Stopwatch started!")) else: tmp = abs(self.stopwatches[author.id] - int(time.perf_counter())) tmp = str(datetime.timedelta(seconds=tmp)) await ctx.send( author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp) ) self.stopwatches.pop(author.id, None) @commands.command() async def lmgtfy(self, ctx, *, search_terms: str): """Create a lmgtfy link.""" search_terms = escape( search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True ) await ctx.send("https://lmgtfy.com/?q={}".format(search_terms)) @commands.command(hidden=True) @commands.guild_only() async def hug(self, ctx, user: discord.Member, intensity: int = 1): """Because everyone likes hugs! Up to 10 intensity levels. """ name = italics(user.display_name) if intensity <= 0: msg = "(っ˘̩╭╮˘̩)っ" + name elif intensity <= 3: msg = "(っ´▽`)っ" + name elif intensity <= 6: msg = "╰(*´︶`*)╯" + name elif intensity <= 9: msg = "(つ≧▽≦)つ" + name elif intensity >= 10: msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name) else: # For the purposes of "msg might not be defined" linter errors raise RuntimeError await ctx.send(msg) @commands.command() @commands.guild_only() @commands.bot_has_permissions(embed_links=True) async def serverinfo(self, ctx, details: bool = False): """ Show server information. `details`: Shows more information when set to `True`. Default to False. """ guild = ctx.guild passed = (ctx.message.created_at - guild.created_at).days created_at = _("Created on {date}. That's over {num} days ago!").format( date=guild.created_at.strftime("%d %b %Y %H:%M"), num=humanize_number(passed), ) online = humanize_number( len([m.status for m in guild.members if m.status != discord.Status.offline]) ) total_users = humanize_number(guild.member_count) text_channels = humanize_number(len(guild.text_channels)) voice_channels = humanize_number(len(guild.voice_channels)) if not details: data = discord.Embed(description=created_at, colour=await ctx.embed_colour()) data.add_field(name=_("Region"), value=str(guild.region)) data.add_field(name=_("Users online"), value=f"{online}/{total_users}") data.add_field(name=_("Text Channels"), value=text_channels) data.add_field(name=_("Voice Channels"), value=voice_channels) data.add_field(name=_("Roles"), value=humanize_number(len(guild.roles))) data.add_field(name=_("Owner"), value=str(guild.owner)) data.set_footer( text=_("Server ID: ") + str(guild.id) + _(" • Use {command} for more info on the server.").format( command=f"{ctx.clean_prefix}serverinfo 1" ) ) if guild.icon_url: data.set_author(name=guild.name, url=guild.icon_url) data.set_thumbnail(url=guild.icon_url) else: data.set_author(name=guild.name) else: def _size(num: int): for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]: if abs(num) < 1024.0: return "{0:.1f}{1}".format(num, unit) num /= 1024.0 return "{0:.1f}{1}".format(num, "YB") def _bitsize(num: int): for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]: if abs(num) < 1000.0: return "{0:.1f}{1}".format(num, unit) num /= 1000.0 return "{0:.1f}{1}".format(num, "YB") shard_info = ( _("\nShard ID: **{shard_id}/{shard_count}**").format( shard_id=humanize_number(guild.shard_id + 1), shard_count=humanize_number(ctx.bot.shard_count), ) if ctx.bot.shard_count > 1 else "" ) # Logic from: https://github.com/TrustyJAID/Trusty-cogs/blob/master/serverstats/serverstats.py#L159 online_stats = { _("Humans: "): lambda x: not x.bot, _(" • Bots: "): lambda x: x.bot, "\N{LARGE GREEN CIRCLE}": lambda x: x.status is discord.Status.online, "\N{LARGE ORANGE CIRCLE}": lambda x: x.status is discord.Status.idle, "\N{LARGE RED CIRCLE}": lambda x: x.status is discord.Status.do_not_disturb, "\N{MEDIUM WHITE CIRCLE}": lambda x: x.status is discord.Status.offline, "\N{LARGE PURPLE CIRCLE}": lambda x: any( a.type is discord.ActivityType.streaming for a in x.activities ), "\N{MOBILE PHONE}": lambda x: x.is_on_mobile(), } member_msg = _("Users online: **{online}/{total_users}**\n").format( online=online, total_users=total_users ) count = 1 for emoji, value in online_stats.items(): try: num = len([m for m in guild.members if value(m)]) except Exception as error: print(error) continue else: member_msg += f"{emoji} {bold(humanize_number(num))} " + ( "\n" if count % 2 == 0 else "" ) count += 1 vc_regions = { "vip-us-east": _("__VIP__ US East ") + "\U0001F1FA\U0001F1F8", "vip-us-west": _("__VIP__ US West ") + "\U0001F1FA\U0001F1F8", "vip-amsterdam": _("__VIP__ Amsterdam ") + "\U0001F1F3\U0001F1F1", "eu-west": _("EU West ") + "\U0001F1EA\U0001F1FA", "eu-central": _("EU Central ") + "\U0001F1EA\U0001F1FA", "europe": _("Europe ") + "\U0001F1EA\U0001F1FA", "london": _("London ") + "\U0001F1EC\U0001F1E7", "frankfurt": _("Frankfurt ") + "\U0001F1E9\U0001F1EA", "amsterdam": _("Amsterdam ") + "\U0001F1F3\U0001F1F1", "us-west": _("US West ") + "\U0001F1FA\U0001F1F8", "us-east": _("US East ") + "\U0001F1FA\U0001F1F8", "us-south": _("US South ") + "\U0001F1FA\U0001F1F8", "us-central": _("US Central ") + "\U0001F1FA\U0001F1F8", "singapore": _("Singapore ") + "\U0001F1F8\U0001F1EC", "sydney": _("Sydney ") + "\U0001F1E6\U0001F1FA", "brazil": _("Brazil ") + "\U0001F1E7\U0001F1F7", "hongkong": _("Hong Kong ") + "\U0001F1ED\U0001F1F0", "russia": _("Russia ") + "\U0001F1F7\U0001F1FA", "japan": _("Japan ") + "\U0001F1EF\U0001F1F5", "southafrica": _("South Africa ") + "\U0001F1FF\U0001F1E6", "india": _("India ") + "\U0001F1EE\U0001F1F3", "dubai": _("Dubai ") + "\U0001F1E6\U0001F1EA", "south-korea": _("South Korea ") + "\U0001f1f0\U0001f1f7", } verif = { "none": _("0 - None"), "low": _("1 - Low"), "medium": _("2 - Medium"), "high": _("3 - High"), "extreme": _("4 - Extreme"), } features = { "PARTNERED": _("Partnered"), "VERIFIED": _("Verified"), "DISCOVERABLE": _("Server Discovery"), "FEATURABLE": _("Featurable"), "COMMUNITY": _("Community"), "PUBLIC_DISABLED": _("Public disabled"), "INVITE_SPLASH": _("Splash Invite"), "VIP_REGIONS": _("VIP Voice Servers"), "VANITY_URL": _("Vanity URL"), "MORE_EMOJI": _("More Emojis"), "COMMERCE": _("Commerce"), "NEWS": _("News Channels"), "ANIMATED_ICON": _("Animated Icon"), "BANNER": _("Banner Image"), "MEMBER_LIST_DISABLED": _("Member list disabled"), } guild_features_list = [ f"✅ {name}" for feature, name in features.items() if feature in guild.features ] joined_on = _( "{bot_name} joined this server on {bot_join}. That's over {since_join} days ago!" ).format( bot_name=ctx.bot.user.name, bot_join=guild.me.joined_at.strftime("%d %b %Y %H:%M:%S"), since_join=humanize_number((ctx.message.created_at - guild.me.joined_at).days), ) data = discord.Embed( description=(f"{guild.description}\n\n" if guild.description else "") + created_at, colour=await ctx.embed_colour(), ) data.set_author( name=guild.name, icon_url="https://cdn.discordapp.com/emojis/457879292152381443.png" if "VERIFIED" in guild.features else "https://cdn.discordapp.com/emojis/508929941610430464.png" if "PARTNERED" in guild.features else discord.Embed.Empty, ) if guild.icon_url: data.set_thumbnail(url=guild.icon_url) data.add_field(name=_("Members:"), value=member_msg) data.add_field( name=_("Channels:"), value=_( "\N{SPEECH BALLOON} Text: {text}\n" "\N{SPEAKER WITH THREE SOUND WAVES} Voice: {voice}" ).format(text=bold(text_channels), voice=bold(voice_channels)), ) data.add_field( name=_("Utility:"), value=_( "Owner: {owner}\nVoice region: {region}\nVerif. level: {verif}\nServer ID: {id}{shard_info}" ).format( owner=bold(str(guild.owner)), region=f"**{vc_regions.get(str(guild.region)) or str(guild.region)}**", verif=bold(verif[str(guild.verification_level)]), id=bold(str(guild.id)), shard_info=shard_info, ), inline=False, ) data.add_field( name=_("Misc:"), value=_( "AFK channel: {afk_chan}\nAFK timeout: {afk_timeout}\nCustom emojis: {emoji_count}\nRoles: {role_count}" ).format( afk_chan=bold(str(guild.afk_channel)) if guild.afk_channel else bold(_("Not set")), afk_timeout=bold(humanize_timedelta(seconds=guild.afk_timeout)), emoji_count=bold(humanize_number(len(guild.emojis))), role_count=bold(humanize_number(len(guild.roles))), ), inline=False, ) if guild_features_list: data.add_field(name=_("Server features:"), value="\n".join(guild_features_list)) if guild.premium_tier != 0: nitro_boost = _( "Tier {boostlevel} with {nitroboosters} boosters\n" "File size limit: {filelimit}\n" "Emoji limit: {emojis_limit}\n" "VCs max bitrate: {bitrate}" ).format( boostlevel=bold(str(guild.premium_tier)), nitroboosters=bold(humanize_number(guild.premium_subscription_count)), filelimit=bold(_size(guild.filesize_limit)), emojis_limit=bold(str(guild.emoji_limit)), bitrate=bold(_bitsize(guild.bitrate_limit)), ) data.add_field(name=_("Nitro Boost:"), value=nitro_boost) if guild.splash: data.set_image(url=guild.splash_url_as(format="png")) data.set_footer(text=joined_on) await ctx.send(embed=data) @commands.command() async def urban(self, ctx, *, word): """Search the Urban Dictionary. This uses the unofficial Urban Dictionary API. """ try: url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower() headers = {"content-type": "application/json"} async with aiohttp.ClientSession() as session: async with session.get(url, headers=headers) as response: data = await response.json() except aiohttp.ClientError: await ctx.send( _("No Urban Dictionary entries were found, or there was an error in the process.") ) return if data.get("error") != 404: if not data.get("list"): return await ctx.send(_("No Urban Dictionary entries were found.")) if await ctx.embed_requested(): # a list of embeds embeds = [] for ud in data["list"]: embed = discord.Embed() embed.title = _("{word} by {author}").format( word=ud["word"].capitalize(), author=ud["author"] ) embed.url = ud["permalink"] description = _("{definition}\n\n**Example:** {example}").format(**ud) if len(description) > 2048: description = "{}...".format(description[:2045]) embed.description = description embed.set_footer( text=_( "{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary." ).format(**ud) ) embeds.append(embed) if embeds is not None and len(embeds) > 0: await menu( ctx, pages=embeds, controls=DEFAULT_CONTROLS, message=None, page=0, timeout=30, ) else: messages = [] for ud in data["list"]: ud.setdefault("example", "N/A") message = _( "<{permalink}>\n {word} by {author}\n\n{description}\n\n" "{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary." ).format(word=ud.pop("word").capitalize(), description="{description}", **ud) max_desc_len = 2000 - len(message) description = _("{definition}\n\n**Example:** {example}").format(**ud) if len(description) > max_desc_len: description = "{}...".format(description[: max_desc_len - 3]) message = message.format(description=description) messages.append(message) if messages is not None and len(messages) > 0: await menu( ctx, pages=messages, controls=DEFAULT_CONTROLS, message=None, page=0, timeout=30, ) else: await ctx.send( _("No Urban Dictionary entries were found, or there was an error in the process.") )