# _____ _ # / ____| (_) # | (___ ___ __ _ _____ ___ _ __ ___ _ __ ___ ___ _ __ # \___ \ / _ \/ _` / __\ \ /\ / / | '_ ` _ \| '_ ` _ \ / _ \ '__| # ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ | # |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_| import random from io import BytesIO import aiohttp import numpy as np from discord import Colour, Embed, File from PIL import Image from red_commons.logging import getLogger from redbot.core import Config, commands, data_manager from redbot.core.bot import Red from redbot.core.utils.chat_formatting import error, humanize_list import bible.errors from bible.models import Version class Bible(commands.Cog): """Retrieve Bible verses from the API.bible API.""" __author__ = ["SeaswimmerTheFsh"] __version__ = "1.1.0" __documentation__ = "https://seacogs.coastalcommits.com/bible/" def __init__(self, bot: Red): super().__init__() self.bot = bot self.session = aiohttp.ClientSession() self.config = Config.get_conf( self, identifier=481923957134912, force_registration=True ) self.logger = getLogger("red.SeaCogs.Bible") self.config.register_global(bible="de4e12af7f28f599-02") self.config.register_user(bible=None) def format_help_for_context(self, ctx: commands.Context) -> str: pre_processed = super().format_help_for_context(ctx) or "" n = "\n" if "\n\n" not in pre_processed else "" text = [ f"{pre_processed}{n}", f"Cog Version: **{self.__version__}**", f"Author: {humanize_list(self.__author__)}", f"Documentation: {self.__documentation__}", ] return "\n".join(text) def get_icon(self, color: Colour) -> File: """Get the docs.api.bible favicon with a given color.""" image_path = data_manager.bundled_data_path(self) / "api.bible-logo.png" image = Image.open(image_path) image = image.convert("RGBA") data = np.array(image) red, green, blue, alpha = data.T # pylint: disable=unused-variable white_areas = (red == 255) & (blue == 255) & (green == 255) data[..., :-1][white_areas.T] = color.to_rgb() image = Image.fromarray(data) with BytesIO() as image_binary: image.save(image_binary, "PNG") image_binary.seek(0) return File(image_binary, filename="icon.png", description="API.Bible Icon") async def translate_book_name(self, bible_id: str, book_name: str) -> str: """Translate a book name to a book ID.""" book_name_list = [ w.lower() if w.lower() == "of" else w.title() for w in book_name.split() ] book_name = " ".join(book_name_list) books = await self._get_books(bible_id) for book in books: if book_name in (book["abbreviation"], book["name"]): return book["id"] raise ValueError(error(f"Book {book_name} not found.")) async def get_version(self, bible_id: str) -> Version: """Retrieve the version of the Bible being used.""" url = f"https://api.scripture.api.bible/v1/bibles/{bible_id}" headers = await self.bot.get_shared_api_tokens("api.bible") async with self.session.get(url, headers=headers) as response: data = await response.json() self.logger.debug( "get_version executed with a response code of: %s", response.status, ) if response.status == 401: raise bible.errors.Unauthorized() if response.status == 403: raise bible.errors.BibleAccessError() if response.status == 503: raise bible.errors.ServiceUnavailable() return Version( bible_id, data["data"]["abbreviation"], data["data"]["language"]["name"], data["data"]["abbreviationLocal"], data["data"]["language"]["nameLocal"], data["data"]["description"], data["data"]["descriptionLocal"], data["data"]["copyright"], ) async def _get_passage( self, ctx: commands.Context, bible_id: str, passage_id: str, include_verse_numbers: bool, ) -> dict: """Get a Bible passage from the API.bible API.""" url = f"https://api.scripture.api.bible/v1/bibles/{bible_id}/passages/{passage_id}" headers = await self.bot.get_shared_api_tokens("api.bible") params = { "fums-version": "3", "content-type": "text", "include-notes": "false", "include-titles": "false", "include-chapter-numbers": "false", "include-verse-numbers": str(include_verse_numbers).lower(), "include-verse-spans": "false", "use-org-id": "false", } async with self.session.get(url, headers=headers, params=params) as response: data = await response.json() self.logger.debug( "_get_passage executed with a response code of: %s", response.status, ) if response.status == 400: raise bible.errors.InexplicableError() if response.status == 401: raise bible.errors.Unauthorized() if response.status == 403: raise bible.errors.BibleAccessError() if response.status == 404: raise bible.errors.NotFound() if response.status == 503: raise bible.errors.ServiceUnavailable() fums_url = "https://fums.api.bible/f3" fums_params = { "t": data["meta"]["fumsToken"], "dId": self.bot.user.id, "sId": ctx.message.created_at.timestamp(), "uId": hash(str(ctx.author.id)), } async with self.session.get(fums_url, params=fums_params) as response: self.logger.debug( "_get_passage FUMS executed with a response code of: %s\nDevice ID: %s\nSession ID: %s\nUser ID: %s (%s)", response.status, self.bot.user.id, ctx.message.created_at.timestamp(), hash(str(ctx.author.id)), ctx.author.id, ) return data["data"] async def _get_books(self, bible_id: str) -> dict: """Get the books of the Bible from the API.bible API.""" url = f"https://api.scripture.api.bible/v1/bibles/{bible_id}/books" headers = await self.bot.get_shared_api_tokens("api.bible") async with self.session.get(url, headers=headers) as response: data = await response.json() self.logger.debug( "_get_books executed with a response code of: %s", response.status, ) if response.status == 401: raise bible.errors.Unauthorized() if response.status == 403: raise bible.errors.BibleAccessError() if response.status == 503: raise bible.errors.ServiceUnavailable() return data["data"] async def _get_chapters(self, bible_id: str, book_id: str) -> dict: """Get the chapters of a book from the API.bible API.""" url = f"https://api.scripture.api.bible/v1/bibles/{bible_id}/books/{book_id}/chapters" headers = await self.bot.get_shared_api_tokens("api.bible") async with self.session.get(url, headers=headers) as response: data = await response.json() self.logger.debug( "_get_chapters executed with a response code of: %s", response.status, ) if response.status == 401: raise bible.errors.Unauthorized() if response.status == 403: raise bible.errors.BibleAccessError() if response.status == 503: raise bible.errors.ServiceUnavailable() return data["data"] async def _get_verses(self, bible_id: str, book_id: str, chapter: int) -> dict: """Get the verses of a chapter from the API.bible API.""" url = f"https://api.scripture.api.bible/v1/bibles/{bible_id}/chapters/{book_id}.{chapter}/verses" headers = await self.bot.get_shared_api_tokens("api.bible") async with self.session.get(url, headers=headers) as response: data = await response.json() self.logger.debug( "_get_verses executed with a response code of: %s", response.status, ) if response.status == 401: raise bible.errors.Unauthorized() if response.status == 403: raise bible.errors.BibleAccessError() if response.status == 503: raise bible.errors.ServiceUnavailable() return data["data"] @commands.group(autohelp=True) async def bible(self, ctx: commands.Context): """Retrieve Bible verses from the API.bible API.""" @bible.command(name="passage", aliases=["verse"]) async def bible_passage(self, ctx: commands.Context, book: str, passage: str): """Get a Bible passage. Example usage: `[p]bible passage Genesis 1:1` `[p]bible passage John 3:16-3:17`""" bible_id = await self.config.bible() try: book_id = await self.translate_book_name(bible_id, book) except ValueError as e: await ctx.send(str(e)) return try: version = await self.get_version(bible_id) if len(passage.split("-")) == 2: from_verse, to_verse = passage.replace(":", ".").split("-") if "." not in to_verse: to_verse = f"{from_verse.split('.')[0]}.{to_verse}" passage = await self._get_passage( ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True ) else: passage = await self._get_passage( ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False ) except ( bible.errors.BibleAccessError, bible.errors.NotFound, bible.errors.InexplicableError, bible.errors.ServiceUnavailable, bible.errors.Unauthorized, ) as e: await ctx.send(e.message) return if len(passage["content"]) > 4096: await ctx.send("The passage is too long to send.") return if await ctx.embed_requested(): icon = self.get_icon(await ctx.embed_color()) embed = Embed( title=f"{passage['reference']}", description=passage["content"].replace("¶ ", ""), color=await ctx.embed_color(), ) embed.set_footer( text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png" ) await ctx.send(embed=embed, file=icon) else: await ctx.send(f"## {passage['reference']}\n{passage['content']}") @bible.command(name="random") async def bible_random(self, ctx: commands.Context): """Get a random Bible verse.""" bible_id = await self.config.bible() try: version = await self.get_version(bible_id) books = await self._get_books(bible_id) book = random.choice(books) chapters = await self._get_chapters(bible_id, book["id"]) chapter = random.choice(chapters) verses = await self._get_verses(bible_id, book["id"], chapter["number"]) verse = random.choice(verses)["id"] passage = await self._get_passage(ctx, bible_id, verse, False) except ( bible.errors.BibleAccessError, bible.errors.NotFound, bible.errors.InexplicableError, bible.errors.ServiceUnavailable, bible.errors.Unauthorized, ) as e: await ctx.send(e.message) return if await ctx.embed_requested(): icon = self.get_icon(await ctx.embed_color()) embed = Embed( title=f"{passage['reference']}", description=passage["content"].replace("¶ ", ""), color=await ctx.embed_color(), ) embed.set_footer( text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png" ) await ctx.send(embed=embed, file=icon) else: await ctx.send(f"## {passage['reference']}\n{passage['content']}")