diff --git a/README.md b/README.md
index f4b9b1c..70014c6 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ A feature-rich Discord leveling bot inspired by Arcane, with support for both **
| **roll** | `/roll [sides]` | `&roll [sides]` | Roll a dice (default d6) |
| **choose** | `/choose ` | `&choose ` | Choose one option from a comma-separated list |
| **rps** | `/rps ` | `&rps ` | Play rock-paper-scissors |
+| **tod** | `/tod ` | `&tod ` | Play truth or dare |
### Info Commands
diff --git a/src/cogs/fun.py b/src/cogs/fun.py
index f9e2054..6eb1d9b 100644
--- a/src/cogs/fun.py
+++ b/src/cogs/fun.py
@@ -102,6 +102,86 @@ async def choose(self, ctx: commands.Context, *, options: str) -> None:
embed = discord.Embed(title="Choice", description=f"I choose: **{pick}**", color=EMBED_COLOR)
await self._send(ctx, embed=embed)
+ @commands.hybrid_command(name="tod", description="Play truth or dare")
+ @app_commands.describe(choice="Truth or dare")
+ @app_commands.choices(
+ choice=[
+ app_commands.Choice(name="Truth", value="truth"),
+ app_commands.Choice(name="Dare", value="dare"),
+ ]
+ )
+ async def tod(self, ctx: commands.Context, choice: str | None = None) -> None:
+ if choice is None:
+ await self._send(
+ ctx,
+ embed=discord.Embed(
+ title="Truth or Dare",
+ description="Pick one: `&tod truth` or `&tod dare` (slash: `/tod`).",
+ color=EMBED_COLOR,
+ ),
+ )
+ return
+ choice_n = choice.strip().lower()
+ truths = [
+ "What is the last thing you searched for on your phone?",
+ "What is your most embarrassing song on your playlist?",
+ "Have you ever pretend-deleted an app just to look busy?",
+ "What is the longest you've gone without showering?",
+ "What is the most embarrassing thing you've accidentally liked on social media?",
+ "What is a secret hobby you've never told anyone about?",
+ "What is the silliest thing you're afraid of?",
+ "Have you ever read the terms and conditions?",
+ "What is your weird food combination that you actually like?",
+ "What is the most childish thing you still do?",
+ "What is the last lie you told?",
+ "If you could swap lives with anyone for a day, who would it be?",
+ "What is the funniest name you've given a pet or a file on your computer?",
+ "Have you ever laughed so hard you cried in public?",
+ "What is your biggest irrational fear?",
+ ]
+ dares = [
+ "Do your best impression of a robot for 10 seconds.",
+ "Speak in an accent until your next turn.",
+ "Let the chat pick your status message for the next hour.",
+ "Do 10 jumping jacks right now.",
+ "Sing the chorus of the first song in your playlist.",
+ "Compliment the person who sent the last message.",
+ "Say 'mmm tasty' after everything you type for 3 messages.",
+ "Act like a cat for the next 2 minutes.",
+ "Type with your eyes closed for one whole message.",
+ "Post the most recent photo in your camera roll (no context).",
+ "Do your best evil villain laugh in voice chat.",
+ "Swap your profile picture with your banner for an hour.",
+ "Try to make the funniest face and send it as an emoji reaction.",
+ "Read your last 5 messages out loud in a dramatic voice.",
+ "Balance something on your head for 30 seconds.",
+ ]
+ if choice_n not in {"truth", "dare"}:
+ await self._send(
+ ctx,
+ embed=discord.Embed(
+ title="Invalid choice",
+ description="Choose **truth** or **dare**.",
+ color=discord.Color.red(),
+ ),
+ ephemeral=True,
+ )
+ return
+
+ if choice_n == "truth":
+ embed = discord.Embed(
+ title="🤫 Truth",
+ description=random.choice(truths),
+ color=EMBED_COLOR,
+ )
+ else:
+ embed = discord.Embed(
+ title="🔥 Dare",
+ description=random.choice(dares),
+ color=EMBED_COLOR,
+ )
+ await self._send(ctx, embed=embed)
+
@commands.hybrid_command(name="rps", description="Play rock-paper-scissors")
@app_commands.describe(choice="Your choice")
async def rps(self, ctx: commands.Context, choice: str) -> None:
diff --git a/src/cogs/help.py b/src/cogs/help.py
index 41571b5..f8a6ae5 100644
--- a/src/cogs/help.py
+++ b/src/cogs/help.py
@@ -141,6 +141,7 @@ def create_fun_help_embed() -> discord.Embed:
{"name": "roll", "usage": "&roll [sides]", "description": "Roll a dice (default d6)"},
{"name": "choose", "usage": "&choose ", "description": "Pick one option from a list"},
{"name": "rps", "usage": "&rps ", "description": "Play rock-paper-scissors"},
+ {"name": "tod", "usage": "&tod ", "description": "Play truth or dare"},
]
for cmd in commands_list:
diff --git a/tests/conftest.py b/tests/conftest.py
index 32855fe..98fc481 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -15,6 +15,45 @@
import pytest
import pytest_asyncio
+# ──────────────────────────────────────────────────────────────────────
+# aiohttp 3.14 / aioresponses compatibility
+# ──────────────────────────────────────────────────────────────────────
+
+
+class _DummyStreamWriter:
+ """Stand-in for aiohttp's stream writer; only ``output_size`` is read."""
+
+ output_size = 0
+
+
+def _patch_client_response_stream_writer() -> None:
+ """Make aioresponses work on aiohttp >= 3.14.
+
+ aiohttp 3.14 made ``stream_writer`` a required keyword-only argument of
+ ``ClientResponse.__init__``, but aioresponses (<= 0.7.9) builds responses
+ without it, so every mocked request raised TypeError
+ (pnuckowski/aioresponses#288, fix unreleased). Supply a stand-in when the
+ caller omits the argument. No-op on aiohttp < 3.14 and once aioresponses
+ passes it itself.
+ """
+ import inspect
+
+ from aiohttp import client_reqrep
+
+ original_init = client_reqrep.ClientResponse.__init__
+ if "stream_writer" not in inspect.signature(original_init).parameters:
+ return
+
+ def _client_response_init(self, *args, **kwargs):
+ kwargs.setdefault("stream_writer", _DummyStreamWriter())
+ original_init(self, *args, **kwargs)
+
+ client_reqrep.ClientResponse.__init__ = _client_response_init
+
+
+_patch_client_response_stream_writer()
+
+
# ──────────────────────────────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────────────────────────────
diff --git a/tests/test_level_service.py b/tests/test_level_service.py
index 0966245..f4eae1c 100644
--- a/tests/test_level_service.py
+++ b/tests/test_level_service.py
@@ -96,7 +96,8 @@ async def test_award_message_xp_cooldown(self, service, mock_discord_message):
"""Test cooldown prevents double XP."""
service.set_cooldown(mock_discord_message.author.id, mock_discord_message.guild.id)
- result = await service.award_message_xp(mock_discord_message)
+ with patch("utils.database.get_guild_settings", AsyncMock(return_value=None)):
+ result = await service.award_message_xp(mock_discord_message)
assert result is None
@pytest.mark.asyncio