def detect_text(url: str) -> str: """Uses Google Cloud VisionAI""" try: # Download image from url # Vision library can take an URL, but this saves an invocation in case of an invalid link response = requests.get(url) # Convert data to image image_bytes = io.BytesIO(response.content) image = vision.types.Image(content=image_bytes.read()) # Invalid URL provided except requests.exceptions.MissingSchema: raise ContentError("That's not an image? {0}{1}\n{2}".format( basic_emoji.get("Pepega"), basic_emoji.get("Clap"), basic_emoji.get("forsenSmug"))) # Let VisionAI do its thing cloud_response = google_vision.text_detection(image=image) texts = cloud_response.text_annotations # Couldn't read anything if not texts: raise ContentError("Can't see shit! {0}".format( basic_emoji.get("forsenT"))) # Return raw detected text return texts[0].description
async def set_icon(self, ctx, emote: str = ""): """Change user's icon to emoji""" if len(emote) == 0: await ctx.send("No emote specified") await ctx.message.add_reaction(basic_emoji.get("Si")) # Find every Discord emote discord_emotes = re.findall(r"<:\w*:\d*>", ctx.message.content) # Unicode emojis compatible by default compatible = extract_emoji(ctx.message.content) # Filter foreign emotes for e in discord_emotes: for known in self.bot.emojis: if e == str(known): compatible.append(e) # Remove duplicates compatible = list(set(compatible)) # If user specified compatible custom emoji if len(compatible) == 1: self.user_icon[ctx.author.id] = str(compatible[0]) await ctx.message.add_reaction("✅") elif len(compatible) == 0: await ctx.send("I can't use that emote " + basic_emoji.get("Sadge")) else: await ctx.send("Too many emotes specified " + basic_emoji.get("Pepega"))
async def decide(self, ctx, *args): """Choose one option from a list""" # No arguments -> exit if not args: await ctx.send( "Decide between what? " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\nUse `;`, `:`, `,` or ` or `, to separate options.") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Join arguments to one string raw = " ".join(str(i) for i in args) # Attempt to split it by any separator options = raw.split(";") if len(options) < 2: options = raw.split(":") if len(options) < 2: options = raw.split(",") if len(options) < 2: options = raw.split(" or ") # Splitting failed if len(options) < 2: await ctx.send( "Separator not recognized, use `;`, `:`, `,` or ` or `, to separate options." ) # Else send a result else: await ctx.send(random.choice(options))
async def read(self, ctx, url: str = ""): """Detect text in image""" # Check whether user provided url or embedded image if not url and not ctx.message.attachments: await ctx.send("Read what? " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\n" + basic_emoji.get("forsenSmug")) await ctx.message.add_reaction(basic_emoji.get("Si")) return # Get url to the image if not url: url = ctx.message.attachments[0].url # Display status status = await ctx.send("Processing... " + basic_emoji.get("docSpin")) # Attempt to detect text try: text = detect_text(url) except ContentError as e: await status.delete() await ctx.send(e) return await status.delete() # Split into short enough segments (Discord's max message length is 2000) for segment in wrap(text, 1990): await ctx.send("```" + segment + "```")
async def joke(self, ctx): """Display a random 'joke'""" url = "http://stupidstuff.org/jokes/joke.htm?jokeid={0}".format(random.randint(1, 3773)) # Attempt to download webpage try: response = requests.get(url, headers) response.raise_for_status() except requests.HTTPError: fail = await ctx.send("Bad response (status code {0}) from {1})".format(response.status_code, url)) await fail.add_reaction(basic_emoji.get("Si")) return # Look for a joke soup = BeautifulSoup(response.content, "html.parser") table = soup.find("table", attrs={"class": "scroll"}) # If element not found if not table: fail = await ctx.send("Joke not found on {0}".format(url)) await fail.add_reaction(basic_emoji.get("Si")) return for row in table.findAll("tr"): # Send string with empty lines removed (split into smaller parts in case it is >2000 characters long) for segment in wrap(str(row.text).replace("\n\n", "\n"), 1990): await ctx.send(segment)
async def select_video(bot: discord.ext.commands.Bot, ctx: discord.ext.commands.Context, query: str) -> str: """Extract Youtube URL, returns empty string if failed""" # If URL contained in argument if "youtube.com/watch?v=" in query or "youtu.be/" in query: # Assuming it's the first 'word' of argument return query.partition(" ")[0] # Else search youtube for video title else: try: videos = youtube_search(query) except ConnectionError: msg = await ctx.send(basic_emoji.get("hackerCD") + "HTTP error. " + basic_emoji.get("Sadge")) await msg.add_reaction(basic_emoji.get("Si")) return "" # 0 videos -> exit if len(videos) == 0: msg = await ctx.send("0 videos found. " + basic_emoji.get("Sadge")) await msg.add_reaction(basic_emoji.get("Si")) return "" # 1 video -> we have a winner elif len(videos) == 1: return "https://www.youtube.com/watch?v=" + videos[0][1] # Else let user to choose which one they meant else: # Only giving 5 choices max number_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"] valid_numbers = [] poll = "" i = 0 # Iterate over all (5 at most) found videos, pair = ('title - channel' : 'video_id') for pair in videos: # Add title to message poll += number_emojis[i] + ". " + pair[0] + "\n" # Add valid option valid_numbers.append(number_emojis[i]) i += 1 # Display message with available videos msg = await ctx.send(poll) await add_choices_message(msg, len(valid_numbers), cancellable=True) # Wait for user to choose choice = await wait_for_choice(bot, ctx.author, msg, valid_numbers, cancellable=True) await msg.delete() # Cancelled or timed out if choice <= 0: return "" return "https://www.youtube.com/watch?v=" + videos[choice - 1][1]
async def pause(self, ctx): """Pause current song""" session = self.sessions[ctx.guild.id] if session.vc.pause(): msg = await ctx.send("Nothing is playing.") await msg.add_reaction(basic_emoji.get("Si")) else: await ctx.send( basic_emoji.get("residentCD") + " Paused " + basic_emoji.get("Okayga"))
async def roll(self, ctx, num: str = "100"): """Roll a dice""" # Default string for invalid input result = "No, I don't think so. " + basic_emoji.get("forsenSmug") # Parse input if num.isnumeric(): # Roll dice result = str(random.randint(1, int(num))) else: await ctx.message.add_reaction(basic_emoji.get("Si")) # Display result await ctx.send(result)
async def markov(self, ctx, *, string: str = ""): """Generate a Markov chain sequence""" # Load model if not loaded if self.markov_model is None: # Decrypt JSON file key = os.getenv("MARKOV_MODEL_KEY") pyAesCrypt.decryptFile("markov_model.json.aes", "markov_model.json", key) # Load model from JSON (expects str, not dict) with open("markov_model.json") as f: self.markov_model = markovify.Text.from_json( json.dumps(json.load(f))) # Generate sentence try: sentence = self.markov_model.make_sentence_with_start( beginning=random.choice(string.split(" ")), tries=1000, min_words=20) except (markovify.text.ParamError, KeyError): sentence = self.markov_model.make_short_sentence(max_chars=2000, min_chars=150, tries=1000) # Return sentence if sentence: await ctx.send(sentence) else: msg = await ctx.send("Failed generating sentence.") await msg.add_reaction(basic_emoji.get("Si"))
async def chan(self, ctx, board: str = "", arg: str = ""): """Display random post (image in spoiler)""" # If no board specified, or random one -> choose random board if not board or board.lower() == "random": board_list = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'gif', 'd', 'h', 'hr', 'k', 'm', 'o', 'p', 'r', 's', 't', 'u', 'v', 'vg', 'w', 'wg', 'i', 'ic', 'r9k', 'cm', 'hm', 'y', '3', 'adv', 'an', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'hc', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'po', 'pol', 'sci', 'soc', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x' ] board = random.choice(board_list) # Attempt to download board try: b = basc_py4chan.Board(board) threads = b.get_all_threads() # Invalid board specified (library uses requests) except requests.exceptions.HTTPError: msg = await ctx.send("`/{0}/` doesn't exist.".format(board)) await msg.add_reaction(basic_emoji.get("Si")) return result = "" post = None # Finding a post with text if arg.lower() == "text" or arg.lower() == "txt": # Try a random one until successful while not post or not post.text_comment: thread = random.choice(threads) post = random.choice(thread.posts) result = post.text_comment # Finding a post with image elif arg.lower() == "image" or arg.lower() == "img": while not post or not post.has_file: thread = random.choice(threads) post = random.choice(thread.posts) # Put image in a spoiler result = "|| {0} ||\n{1}".format(post.file_url, post.text_comment) # If no option specified -> find a post with text, image optional else: while not post or not post.text_comment: thread = random.choice(threads) post = random.choice(thread.posts) if post.has_file: result = "|| {0} ||\n{1}".format(post.file_url, post.text_comment) else: result = post.text_comment # Split into smaller parts if a post is too long (>2000 characters) for segment in wrap(result, 1990): await ctx.send(segment)
async def verbose_garfield(ctx, date) -> None: """Send a Garfield strip, with status notification""" status = await ctx.send( basic_emoji.get("hackerCD") + " Searching for Garfield strip " + basic_emoji.get("docSpin")) try: comic = garfield_strip(date) except GarfieldError as e: await status.delete() await ctx.send(e) return await status.delete() await ctx.send(comic)
async def rand_date(self, ctx): """Display random Garfield strip + interesting fact about that day""" date = random_date(datetime.datetime(1978, 6, 19), datetime.datetime.utcnow()) await verbose_garfield(ctx, date) status = await ctx.send("Looking up an interesting fact... " + basic_emoji.get("docSpin")) msg = "This comic came out in " + custom_strftime("%B {S}, %Y", date) + "." # Try to find an interesting fact try: fact = get_day_fact(date) # Error -> stop except WikipediaError as e: await status.delete() await ctx.send(msg + e.message) return await status.delete() response = await ctx.send(msg + " Also on this day in " + fact) await response.add_reaction(random.choice(scoots_emoji))
async def ping(self, ctx): """Displays time delta between Discord message and command invocation""" ms = (datetime.utcnow() - ctx.message.created_at.replace(tzinfo=None) ).total_seconds() * 1000 await ctx.send( basic_emoji.get("Pepega") + " 🏓 Pong! `{0}ms`".format(int(ms)))
async def weather(self, ctx, *args): """Get weather information""" # Default location location = "Prague" if args: location = " ".join(str(i) for i in args) url = ("http://api.openweathermap.org/data/2.5/weather?q=" + location + "&units=metric&lang=en&appid=" + WEATHER_TOKEN) res = requests.get(url).json() if str(res["cod"]) == "200": description = "Weather in " + res["name"] + " , " + res["sys"][ "country"] embed = discord.Embed(title="Weather", description=description) image = "http://openweathermap.org/img/w/" + res["weather"][0][ "icon"] + ".png" embed.set_thumbnail(url=image) weather = res["weather"][0]["main"] + " (" + res["weather"][0][ "description"] + ") " temp = str(res["main"]["temp"]) + "°C" feels_temp = str(res["main"]["feels_like"]) + "°C" humidity = str(res["main"]["humidity"]) + "%" wind = str(res["wind"]["speed"]) + "m/s" clouds = str(res["clouds"]["all"]) + "%" visibility = str( res["visibility"] / 1000) + " km" if "visibility" in res else "no data" embed.add_field(name="Weather", value=weather, inline=False) embed.add_field(name="Temperature", value=temp, inline=True) embed.add_field(name="Feels like", value=feels_temp, inline=True) embed.add_field(name="Humidity", value=humidity, inline=True) embed.add_field(name="Wind", value=wind, inline=True) embed.add_field(name="Clouds", value=clouds, inline=True) embed.add_field(name="Visibility", value=visibility, inline=True) await ctx.send(embed=embed) elif str(res["cod"]) == "404": msg = await ctx.send("Location not found.") await msg.add_reaction(basic_emoji.get("Sadge")) elif str(res["cod"]) == "401": msg = await ctx.send("API key broke, have a nice day.") await msg.add_reaction(basic_emoji.get("Si")) else: await ctx.send("Location not found! " + basic_emoji.get("Sadge") + " (" + res["message"] + ")")
async def skip(self, ctx): """Play next song in queue""" session = self.sessions[ctx.guild.id] if not session.next_song(): msg = await ctx.send("Nothing is playing.") await msg.add_reaction(basic_emoji.get("Si"))
async def stop(self, ctx): """Stop playback""" session = self.sessions[ctx.guild.id] if not session.stop(): msg = await ctx.send("Nothing is playing.") await msg.add_reaction(basic_emoji.get("Si"))
async def on_command_error(ctx, error): """Executed when an exception is raised""" # Unknown command if isinstance(error, commands.CommandNotFound): await ctx.message.add_reaction(basic_emoji.get("Si")) await ctx.send("{0}📣 COMMAND NOT FOUND".format( basic_emoji.get("Pepega"))) # Limited command in DMs elif isinstance(error, commands.errors.NoPrivateMessage): await ctx.message.add_reaction(basic_emoji.get("Si")) await ctx.send("Not available in DMs.") # Unescaped quotes elif isinstance(error, commands.errors.UnexpectedQuoteError): await ctx.message.add_reaction(basic_emoji.get("Si")) await ctx.send( "{0}📣 UNEXPECTED QUOTE ERROR\nUse `\\` to escape your quote(s) {1}" .format(basic_emoji.get("Pepega"), basic_emoji.get("forsenScoots"))) # Command cooldown elif isinstance(error, commands.CommandOnCooldown): await ctx.message.add_reaction(basic_emoji.get("Si")) await ctx.send("That command is on cooldown.") else: raise error
async def forceplay(self, ctx, *args): """Put song in front of queue""" session = self.sessions[ctx.guild.id] # No arguments -> exit if not args: await ctx.send("Play what? " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\n" + basic_emoji.get("forsenSmug")) await ctx.message.add_reaction(basic_emoji.get("Si")) return if session.vc is None: await ctx.send("Nothing is queued to skip in front of " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\nUse `p.play`") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Extract youtube video url arg = " ".join(str(i) for i in args) url = await select_video(self.bot, ctx, arg) # No video selected by user if not url: return session.forceplay(url) await ctx.send("Song inserted to the front of the queue.")
def detect_text(url: str) -> str: """Uses Google Cloud VisionAI""" # Check if link is an image mimetype, encoding = mimetypes.guess_type(url) if not mimetype or not mimetype.startswith("image"): raise ContentError("That's not an image? {0}{1}\n{2}".format( basic_emoji.get("Pepega"), basic_emoji.get("Clap"), basic_emoji.get("forsenSmug"))) # Download image try: response = requests.get(url) image_bytes = io.BytesIO(response.content) image = vision_v1.types.Image(content=image_bytes.read()) except requests.exceptions.MissingSchema: raise ContentError("That's not an image? {0}{1}\n{2}".format( basic_emoji.get("Pepega"), basic_emoji.get("Clap"), basic_emoji.get("forsenSmug"))) # Read image cloud_response = google_vision.text_detection(image=image) texts = cloud_response.text_annotations # Couldn't read anything if not texts: raise ContentError("Can't see shit! {0}".format( basic_emoji.get("forsenT"))) # Return raw detected text return texts[0].description
async def fact(self, ctx, arg1: str = "", arg2: str = ""): """Displays a random interesting fact""" # No date provided -> use today's date if not arg1 or not arg2: date = datetime.datetime.today() msg = "On this day in the year " # Invalid date input -> stop elif not arg1.isnumeric() or not arg2.isnumeric(): await ctx.send( "That's not even a numeric date. Try something like '9 11'.") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Attempt to parse date input else: try: date = datetime.date(2000, int(arg1), int(arg2)) msg = "On " + custom_strftime("%B {S}", date) + " in the year " except ValueError: await ctx.send( "No..? You must be using the wrong calendar. Try 'Month Day'." ) await ctx.message.add_reaction(basic_emoji.get("Si")) return status = await ctx.send("Looking up an interesting fact... " + basic_emoji.get("docSpin")) # Try to find an interesting fact try: fact = get_day_fact(date) # Error -> stop except WikipediaError as e: await status.delete() await ctx.send(e) return await status.delete() response = await ctx.send(msg + fact) await response.add_reaction(random.choice(scoots_emoji))
async def repeat(self, ctx): """Toggle repeat""" session = self.sessions[ctx.guild.id] if not session.song: msg = await ctx.send("Nothing is playing.") await msg.add_reaction(basic_emoji.get("Si")) return session.repeat = not session.repeat await ctx.send("Repeat set to `{0}`".format(session.repeat))
async def clear(self, ctx): """Clear song queue""" session = self.sessions[ctx.guild.id] if not session.song_queue: await ctx.send("Queue already empty " + basic_emoji.get("forsenScoots")) return session.song_queue = [] await ctx.send("Queue emptied.")
async def garf(self, ctx, arg1: str = "", arg2: str = "", arg3: str = ""): """Get specific Garfield comic""" # Parsing input if not arg1 or not arg2 or not arg3: await ctx.send( basic_emoji.get("forsenT") + " Date looks like 'Year Month Day', ie. '2001 9 11'.") await ctx.message.add_reaction(basic_emoji.get("Si")) return if not arg1.isnumeric() or not arg2.isnumeric() or not arg3.isnumeric( ): await ctx.send( basic_emoji.get("forsenT") + " That's not even a numeric date.") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Construct date try: date = datetime.datetime(int(arg1), int(arg2), int(arg3)) except ValueError: await ctx.send( basic_emoji.get("forsenSmug") + " No..? You must be using the wrong calendar.") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Send comic strip await verbose_garfield(ctx, date)
async def decide(self, ctx, *args): """Choose one option from a list""" separators = [';', ':', ',', ' or ', ' '] hint = "" # Add separators in reverse priority for sep in reversed(separators[1:]): hint += f"`{sep}`, " # Add "or" in front of last separator hint += f"or `{separators[0]}`" # No arguments -> exit if not args: await ctx.send("Decide between what? " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\nUse " + hint + " to separate options.") await ctx.message.add_reaction(basic_emoji.get("Si")) return # Join arguments to one string raw = " ".join(str(i) for i in args) # Attempt to split it by any separator options = [] for separator in separators: options = raw.split(separator) if len(options) > 1: break # Splitting failed, show hint if len(options) < 2: await ctx.send( f"Separator not recognized, use one of {hint} to separate options." ) # Else send a result else: await ctx.send(random.choice(options))
async def translate(self, ctx, *, arg: str = ""): """Translate text""" # No text entered -> nothing to translate if not arg: await ctx.send("Translate what? " + basic_emoji.get("Pepega") + basic_emoji.get("Clap") + "\n" + basic_emoji.get("forsenSmug")) await ctx.message.add_reaction(basic_emoji.get("Si")) return # Split into ["first_word", "the_rest of the query"] query = arg.split(" ", 1) # If first word is an ISO639-1 language code, translate to that language if query[0] in googletrans.LANGUAGES: result = translator.translate(query[1], dest=query[0]) # Otherwise translate to english by default else: result = translator.translate(arg, dest="en") # Using .lower() because for example chinese-simplified is 'zh-cn', but result.src returns 'zh-CN' (so dumb) header = "Translated from `{0}` {1} to `{2}` {3}".format( googletrans.LANGUAGES.get(result.src.lower()), code_to_country(result.src.lower()), googletrans.LANGUAGES.get(result.dest.lower()), code_to_country(result.dest.lower())) # Split into parts in case of very long translation first_iter = True for segment in wrap(result.text, 1950): # Send header together with the first part if first_iter: await ctx.send("{0}\n```{1}```".format(header, segment)) first_iter = False # Send the rest parts standalone else: await ctx.send("```" + segment + "```")
async def wolfram(self, ctx, *args): """Ask WolframAlpha a question""" # No arguments -> exit if not args: await ctx.send("What? " + basic_emoji.get('Pepega') + basic_emoji.get('Clap')) await ctx.message.add_reaction(basic_emoji.get('Si')) return # Parse query into url-friendly format (for example replaces spaces with '%2') query = urllib.parse.quote_plus(" ".join(str(i) for i in args)) # Send query (with some extra arguments regarding result formatting) url = "http://api.wolframalpha.com/v1/simple?appid={0}&i={1}&background=36393f&foreground=white&timeout=30".format( WOLFRAM_APPID, query) async with ctx.typing(): # Attempt to download result try: response = requests.get(url) response.raise_for_status() # Invalid query / timeout / something else except requests.HTTPError: fail = await ctx.send("Bad response (status code: {0})".format( response.status_code)) await fail.add_reaction(basic_emoji.get("Si")) return # I want to send an image (generated by WolframAlpha), not embed a link (image would be regenerated if user clicked it + it would contain app_id) # And because discord.File has to open the file, I first save the file, then embed it, then delete it... # And to avoid overwriting during simultaneous calls, use the query's hash as the filename filename = "tmp" + str(hash(query)) open(filename, "wb").write(response.content) await ctx.send(file=discord.File(filename, filename="result.png")) os.remove(filename)
async def playing(self, ctx): """Display currently playing song""" session = self.sessions[ctx.guild.id] if not session.song: msg = await ctx.send("Nothing is playing.") await msg.add_reaction(basic_emoji.get("Si")) else: title = await ctx.send( random.choice(dance_emoji) + " 🎶 Now playing 🎶: " + session.song) await title.add_reaction(random.choice(dance_react))
def get_day_fact(date: datetime.datetime) -> str: # Try to find an interesting fact try: raw = wikipedia.page(date.strftime("%B") + " " + str(date.day)).section("Events") # Library returns long string or None facts = raw.splitlines() # Returned None -> error -> stop except AttributeError: raise WikipediaError("No facts found on wikipedia.com/wiki/" + date.strftime("%B") + "_" + str(date.day) + " " + basic_emoji.get("Pepega")) # Returned empty string if not facts: raise WikipediaError("No facts found on wikipedia.com/wiki/" + date.strftime("%B") + "_" + str(date.day) + " " + basic_emoji.get("Pepega")) # Choose a random line (each line is 1 fact) else: return random.choice(facts)
async def tomorrow(self, ctx): """Display when tomorrow's Garfield comic comes out""" # Calculate timedelta delta = next_garfield() hours = delta.seconds // 3600 % 24 minutes = delta.seconds // 60 % 60 seconds = delta.seconds - hours * 3600 - minutes * 60 # If next garfield actually comes out today, simply add 24 hours for tomorrow's now = datetime.datetime.utcnow() # TODO: Add summer/wintertime offset if now.hour < 6 or (now.hour == 6 and now.minute < 7): hours += 24 await ctx.message.add_reaction(basic_emoji.get("Si")) await ctx.send( "You will have to be patient, tomorrow's comic comes out in {0}:{1}:{2}." .format( str(hours).zfill(2), str(minutes).zfill(2), str(seconds).zfill(2)))
async def created(self, ctx, user: Union[nextcord.Member, nextcord.User, nextcord.ClientUser, str, None]): """Display account creation date""" if isinstance(user, str): await ctx.send("Try a user's tag instead " + basic_emoji.get("Okayga")) return if user is None: user_id = ctx.author.id msg = "Your account" else: user_id = user.id msg = "That account" # Decode user's ID binary = str(bin(user_id)[2:]) unix_binary = binary[:len(binary) - 22] unix = (int(unix_binary, 2) + 1420070400000) // 1000 time = datetime.utcfromtimestamp(unix).strftime("%Y-%m-%d %H:%M:%S") await ctx.send("{0} was created at {1} UTC".format(msg, time))