def test_options_list_base_command(command): command.name = "name" clazz = SlashCommand(command=command, root=None, name=None, description="description", options=[Option(name="opt", description="desc")]) assert clazz.options == [Option(name="opt", description="desc")]
def test_options_empty_base_command_group_options(command): command.name = "name" clazz = SlashCommand(command=command, root=None, name=None, description="description", options=[]) clazz.append_options([Option(name="opt", description="desc")]) assert clazz.options == [Option(name="opt", description="desc")]
class EightBall(commands.Cog): def __init__(self, bot): self.bot = bot @slash_command(options=[ Option(name="question", description="The question to ask the magic 8 ball.") ]) @commands.command(name="eightball", aliases=["8ball"], description="Ask the magic 8 ball a question!") async def eightball_command(self, context, *, question: str = None): await self.eightball(context, question) async def eightball(self, context: commands.Context, question: str): if question is None: await context.send( "You need to ask a question to get an answer. :unamused:") elif len(question.replace("?", "")) == 0: await context.send( "Who do you think you are? I AM!\nhttps://youtu.be/gKQOXYB2cd8?t=10" ) elif not question.endswith("?"): await context.send("I can't tell if that's a question, brother.") else: async with context.typing(): if random.random() < 3.0 / 10.0: await asyncio.sleep(3.0) await context.send(random.choice(joke_phrases)) await asyncio.sleep(5.0) await context.send(embed=Embed(colour=Colour.purple( )).add_field( name= f"{context.author.display_name}, my :crystal_ball: says:", value=f"_{random.choice(phrases)}_"))
def test_to_dict(): assert Option(name="n", description="d", type=OptionType.STRING, required=True).to_dict() == { "name": "n", "description": "d", "type": OptionType.STRING, "required": True, "options": [], }
def test_slash_command_slash_options_subcommand(): options = [Option(name="opt", description="desc")] slash = slash_command(root="root", name="name", options=options)(command()(func)) assert slash.slash_ext.options == [ SubCommand(name="name", description=None, type=OptionType.SUB_COMMAND, options=options) ]
async def test_upsert_slash_commands_create_subcommand_group_not_prod( bot, http, guild, command, autospec): command2 = autospec.of("discord.ext.commands.Command") create_slash_command(command, "first", name="1", root="root", options=[Option(name="1", description="d1")]) create_slash_command(command2, "second", name="2", root="root", options=[Option(name="2", description="d2")]) expected = command.slash_ext expected.append_options(command2.slash_ext.options) bot.walk_commands.return_value = [command, command2] bot.guilds = [guild] clazz = SlashCommandHandler(bot) await clazz.upsert_slash_commands() bot.http.bulk_upsert_global_commands.assert_called_once_with( bot.user.id, [expected.to_dict()]) bot.http.bulk_upsert_guild_commands.assert_called_once_with( bot.user.id, guild.id, [expected.to_dict()])
async def test_upsert_slash_commands_create_subcommand_not_prod( bot, http, guild, command): create_slash_command(command, "command_name", name="name", root="root", options=[Option(name="opt", description="d")]) bot.walk_commands.return_value = [command] bot.guilds = [guild] clazz = SlashCommandHandler(bot) await clazz.upsert_slash_commands() bot.http.bulk_upsert_global_commands.assert_called_once_with( bot.user.id, [command.slash_ext.to_dict()]) bot.http.bulk_upsert_guild_commands.assert_called_once_with( bot.user.id, guild.id, [command.slash_ext.to_dict()])
class DogPhotos(commands.Cog): def __init__(self, bot): self.bot = bot @slash_command(options=[ Option(name="breed", description= "The specific breed of dog to show. Defaults to any breed.") ]) @commands.command(name="dog", aliases=["doge"], description="Show a random dog you probably don't know") async def dog_command(self, context, *, breed: Optional[str] = None): await self.dog(context, breed) async def dog(self, context, breed: Optional[str]): if breed and breed in self.get_breeds(): await context.send(self.get_dog_image(breed)) else: await context.send(self.get_dog_image(None)) def get_dog_image(self, breed: Optional[str] = None) -> str: if breed: if " " in breed: path = f"breed/{'/'.join(reversed(breed.split()))}/images" else: path = f"breed/{breed}/images" else: path = f"breed/{breed.replace(' ', '/')}/images" if breed else "breeds/image" result = requests.get(f"https://dog.ceo/api/{path}/random").json() if result.get("status", "ded") != "success" or not result.get( "message", None): raise RuntimeError(f"could not fetch a puppy; breed = {breed}") else: return result.get("message") def get_breeds(self) -> List[str]: result = requests.get("https://dog.ceo/api/breeds/list/all").json() if result.get("status", "ded") != "success" or not result.get( "message", None): raise RuntimeError("could not fetch a puppy") else: breeds = [] for breed, sub_breeds in result.get("message").items(): breeds.append(breed) for sub in sub_breeds: breeds.append(f"{sub} {breed}") return breeds
class Dice(commands.Cog): def __init__(self, bot): self.bot = bot @slash_command(options=[ Option( name="expression", description="The number and type of dice to roll. Default is 1d20") ]) @commands.command(name="roll", aliases=["r"], description="Roll some Dungeons & Dragons style dice!") async def roll_command(self, context, *, expression: str = "1d20"): await self.roll(context, expression) async def roll(self, context, expression: str): max_length = MAX_MESSAGE_LENGTH - 50 # max-50 as a buffer for the added text try: roller = self.make_roller(100_000) result = roller.roll(expression, allow_comments=True, stringifier=DiceStringifier()) text = f"{result.result[:max_length]}..." if len( result.result) > max_length else result.result await context.send(f"**Rolls**: {text}\n**Total**: {result.total}") except d20.errors.TooManyRolls: await context.send( f"I can only roll up to {roller.context.max_rolls} dice.", delete_after=30) except d20.errors.RollError as e: await context.send( f"Oh... :nauseated_face: I don't feel so good... :face_vomiting:\n```{e}```", delete_after=30) def make_roller(self, max_rolls: int): return d20.Roller(d20.RollContext(max_rolls))
def test_name_contains_whitespace_throws(name): with pytest.raises(ValueError): Option(name=name, description="d")
def test_slash_command_slash_options_is_provided(): options = [Option(name="opt", description="desc")] slash = slash_command(options=options)(command()(func)) assert slash.slash_ext.options == options
import pytest from duckbot.slash import Option, OptionType from duckbot.slash.option import SubCommand option = Option(name="a", description="b", type=OptionType.STRING, required=True) @pytest.mark.parametrize("name", ["a name", "some\nname", "bruh\tname"]) def test_name_contains_whitespace_throws(name): with pytest.raises(ValueError): SubCommand(name=name, description="d", options=[option]) def test_to_dict(): assert SubCommand(name="n", description="d", type=OptionType.SUB_COMMAND, options=[option]).to_dict() == { "name": "n", "description": "d", "type": OptionType.SUB_COMMAND, "required": False, "options": [option.to_dict()], }
class Recipe(commands.Cog): def __init__(self, bot): self.bot = bot @staticmethod def select_recipe(recipe_list): """Given a list of recipes, select a random one.""" return random.choice(recipe_list) @staticmethod def parse_recipes(html_content): """Parse raw HTML from allrecipes to find a list of recipes.""" recipe_list = [] soup = BeautifulSoup(html_content, "html.parser") articles = soup.findAll( "div", {"class": "component card card__recipe card__facetedSearchResult"}) for article in articles: data = {} try: data["name"] = article.find("h3", { "class": "card__title" }).get_text().strip(" \t\n\r") data["description"] = article.find("div", { "class": "card__summary" }).get_text().strip(" \t\n\r") data["url"] = article.find( "a", href=re.compile( r"^https://www\.allrecipes\.com/recipe/"))["href"] data["rating"] = len( article.findAll("span", {"class": "rating-star active"})) recipe_list.append(data) except Exception: pass return recipe_list @staticmethod def search_recipes(search_term): """Search allrecipes with a given search term then return html data.""" html_content = "" for page in range(1, 6): url = "https://www.allrecipes.com/element-api/content-proxy/faceted-searches-load-more" result = requests.get(url, params={ "search": search_term, "page": page }, headers={ "Cookie": "euConsent=true" }).json() html_content += result.get("html", "") if not result.get("hasNext", False): break return html_content async def recipe(self, context, search_term): # clean up the arguments to make a valid recipe search search_term = re.sub(r"[^\w\s]", "", search_term) try: # search for recipes on allrecipes.com html_content = self.search_recipes(search_term) # parse the html to get all recipes from the search recipe_list = self.parse_recipes(html_content) if len(recipe_list) == 0: response = f"I am terribly sorry. There doesn't seem to be any recipes for {search_term}." else: recipe = self.select_recipe(recipe_list) response = f"How about a nice {recipe['name']}. {recipe['description']} This recipe has a {recipe['rating']}/5 rating! {recipe['url']}" except Exception: response = "I am terribly sorry. I am having problems reading All Recipes for you." await context.send(response) @slash_command(options=[ Option(name="search", description="Search terms for the recipe") ]) @commands.command(name="recipe", description="Get a random recipe for something.") async def recipe_command(self, context, *, search_term: str = ""): async with context.typing(): await self.recipe(context, search_term)
class Dictionary(commands.Cog): def __init__(self, bot): self.bot = bot self.headers = {"app_id": os.getenv("OXFORD_DICTIONARY_ID"), "app_key": os.getenv("OXFORD_DICTIONARY_KEY")} self.url = "https://od-api.oxforddictionaries.com/api/v2" @slash_command(options=[Option(name="word", description="The word to define.", required=True)]) @commands.command(name="define", description="Define a brother, word.") async def define_command(self, context, *, word: str = "taco"): await self.define(context, word) async def define(self, context, word: str): roots = self.get_root_words(word.lower()) or ["why"] await context.send(embeds=[self.get_definition(x) for x in roots]) def get_root_words(self, word: str) -> List[str]: """Returns all roots of the given word.""" response = requests.get(f"{self.url}/lemmas/en/{word}", headers=self.headers).json() results = response.get("results", []) inflections = [i.get("id") for r in results for lex in r.get("lexicalEntries", []) for i in lex.get("inflectionOf", [])] return sorted(set(inflections)) def get_definition(self, word: str) -> discord.Embed: """Returns the full definition of the word as an embed.""" embed = discord.Embed(title=word) response = requests.get(f"{self.url}/entries/en-us/{word}", headers=self.headers).json() results = response.get("results", []) categories = sorted(set(lex.get("lexicalCategory", {}).get("id", None) for r in results for lex in r.get("lexicalEntries", []))) for category in categories: text, pronunciation, lines = self.category_group_data(word, category, results) embed.add_field(name=f"{category}: **{text}** /{pronunciation}/", value="\n".join(lines), inline=False) return embed def category_group_data(self, word: str, category: str, results: List[dict]) -> Tuple[str, str, List[str]]: lines = [] n = 0 text = word pronunciation = "screw flanders" for result in results: for lex in result.get("lexicalEntries", []): if lex.get("lexicalCategory", {}).get("id", None) == category: text, pronunciation, lex_lines, count = self.definition_data(word, lex, n) n += count lines = lines + lex_lines return text, pronunciation, lines def definition_data(self, word: str, lexical_entry: dict, entry_number: int) -> Tuple[str, str, List[str], int]: lines = [] text = lexical_entry.get("text", word) pronunciation = "screw flanders" n = entry_number for entry in lexical_entry.get("entries", []): pronunciation, entry_lines, count = self.entry_data(word, entry, n) n += count lines = lines + entry_lines return text, pronunciation, lines, n def entry_data(self, word: str, entry: dict, entry_number: int) -> Tuple[str, List[str], int]: lines = [] n = entry_number pronunciation = next((x.get("phoneticSpelling", "") for x in entry.get("pronunciations", []) if x.get("phoneticNotation", "") == "respell"), "") for sense in entry.get("senses", []): n += 1 definition = next(iter(sense.get("definitions", [])), "it means things") example = next(iter(sense.get("examples", [])), {}).get("text", f"this is where I'd use {word} in a sentence... IF I HAD ONE") lines.append(f"{n}. {definition}\n_{example}_") if example else lines.append(f"{n}. {definition}") for sub in sense.get("subsenses", []): definition = next(iter(sub.get("definitions", [])), "") example = next(iter(sub.get("examples", [])), {}).get("text", "") if definition: lines.append(f" • {definition}\n _{example}_") if example else lines.append(f" • {definition}") lines.append("") return pronunciation, lines, n