Skip to content

Commit 26c3664

Browse files
mhaxanalijb3
andauthored
Add .quote daily and .quote random command to retrieve quotes from zenquotes.io api (#1707)
* feat: Add .quote and .daily_quote command to retrieve a random quote and the daily quote from the zenquotes.io api respectively * Implement caching for daily quote to minimize API requests. --------- Co-authored-by: Joe Banks <joe@jb3.dev>
1 parent 90b332d commit 26c3664

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

bot/exts/fun/fun.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Literal
66

77
import pyjokes
8+
from aiohttp import ClientError, ClientResponseError
89
from discord import Embed
910
from discord.ext import commands
1011
from discord.ext.commands import BadArgument, Cog, Context
@@ -14,6 +15,7 @@
1415
from bot.bot import Bot
1516
from bot.constants import Client, Colours, Emojis
1617
from bot.utils import helpers, messages
18+
from bot.utils.quote import daily_quote, random_quote
1719

1820
log = get_logger(__name__)
1921

@@ -158,6 +160,57 @@ async def joke(self, ctx: commands.Context, category: Literal["neutral", "chuck"
158160
joke = pyjokes.get_joke(category=category)
159161
await ctx.send(joke)
160162

163+
@commands.group(name="quote")
164+
async def quote(self, ctx: Context) -> None:
165+
"""
166+
Retrieve a quote from zenquotes.io api.
167+
168+
See `random`, `daily` subcommands.
169+
"""
170+
if ctx.invoked_subcommand is None:
171+
await ctx.invoke(self.bot.get_command("help"), "quote")
172+
173+
@quote.command(name="daily")
174+
async def quote_daily(self, ctx: Context) -> None:
175+
"""Retrieve the daily quote from zenquotes.io api."""
176+
try:
177+
quote = await daily_quote(self.bot)
178+
embed = Embed(
179+
title="Daily Quote",
180+
description=f"> {quote}\n\n-# Powered by [zenquotes.io](https://zenquotes.io)",
181+
colour=Colours.blue
182+
)
183+
await ctx.send(embed=embed)
184+
except ClientResponseError as e:
185+
log.warning(f"ZenQuotes API error: {e.status} {e.message}")
186+
await ctx.send(":x: Could not retrieve quote from API.")
187+
except (ClientError, TimeoutError) as e:
188+
log.error(f"Network error fetching quote: {e}")
189+
await ctx.send(":x: Could not connect to the quote service.")
190+
except Exception:
191+
log.exception("Unexpected error fetching quote.")
192+
await ctx.send(":x: Something unexpected happened. Try again later.")
193+
194+
@quote.command(name="random")
195+
async def quote_random(self, ctx: Context) -> None:
196+
"""Retrieve a random quote from zenquotes.io api."""
197+
try:
198+
quote = await random_quote(self.bot)
199+
embed = Embed(
200+
title="Random Quote",
201+
description=f"> {quote}\n\n-# Powered by [zenquotes.io](https://zenquotes.io)",
202+
colour=Colours.blue
203+
)
204+
await ctx.send(embed=embed)
205+
except ClientResponseError as e:
206+
log.warning(f"ZenQuotes API error: {e.status} {e.message}")
207+
await ctx.send(":x: Could not retrieve quote from API.")
208+
except (ClientError, TimeoutError) as e:
209+
log.error(f"Network error fetching quote: {e}")
210+
await ctx.send(":x: Could not connect to the quote service.")
211+
except Exception:
212+
log.exception("Unexpected error fetching quote.")
213+
await ctx.send(":x: Something unexpected happened. Try again later.")
161214

162215
async def setup(bot: Bot) -> None:
163216
"""Load the Fun cog."""

bot/utils/quote.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Utility functions for fetching quotes from ZenQuotes API."""
2+
3+
from datetime import UTC, datetime, timedelta
4+
5+
from pydis_core.utils.logging import get_logger
6+
7+
from bot.bot import Bot
8+
9+
RANDOM_QUOTE_URL = "https://zenquotes.io/api/random"
10+
DAILY_QUOTE_URL = "https://zenquotes.io/api/today"
11+
DAILY_QUOTE_KEY="daily_quote"
12+
13+
log = get_logger(__name__)
14+
15+
def seconds_until_midnight_utc() -> int:
16+
"""Calculate the number of seconds remaining until midnight UTC for Redis cache TTL."""
17+
now = datetime.now(UTC)
18+
tomorrow = now + timedelta(days=1)
19+
midnight = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
20+
time_to_midnight = (midnight - now)
21+
return int(time_to_midnight.total_seconds())
22+
23+
24+
async def random_quote(bot: Bot) -> str:
25+
"""Retrieve a random quote from ZenQuotes API."""
26+
async with bot.http_session.get(RANDOM_QUOTE_URL) as response:
27+
response.raise_for_status()
28+
data = await response.json()
29+
quote = f"{data[0]['q']}\n*— {data[0]['a']}*"
30+
return quote
31+
32+
33+
async def daily_quote(bot: Bot) -> str:
34+
"""Retrieve the daily quote from ZenQuotes API, cached until 00:00 UTC."""
35+
redis = bot.redis_session.client
36+
37+
cached_quote = await redis.get(DAILY_QUOTE_KEY)
38+
if cached_quote:
39+
log.debug("Using cached daily quote.")
40+
return cached_quote
41+
42+
log.debug("No cached quote found.")
43+
async with bot.http_session.get(DAILY_QUOTE_URL) as resp:
44+
resp.raise_for_status()
45+
data = await resp.json()
46+
quote = f"{data[0]['q']}\n*— {data[0]['a']}*"
47+
48+
ttl = seconds_until_midnight_utc()
49+
50+
await redis.set(DAILY_QUOTE_KEY, quote, ex=ttl)
51+
log.info(f"Cached daily quote for {ttl} seconds.")
52+
53+
return quote

0 commit comments

Comments
 (0)