Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a, b, c>` | `&choose <a, b, c>` | Choose one option from a comma-separated list |
| **rps** | `/rps <choice>` | `&rps <choice>` | Play rock-paper-scissors |
| **tod** | `/tod <choice>` | `&tod <choice>` | Play truth or dare |

### Info Commands

Expand Down
80 changes: 80 additions & 0 deletions src/cogs/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/cogs/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a, b, c>", "description": "Pick one option from a list"},
{"name": "rps", "usage": "&rps <rock|paper|scissors>", "description": "Play rock-paper-scissors"},
{"name": "tod", "usage": "&tod <truth|dare>", "description": "Play truth or dare"},
]

for cmd in commands_list:
Expand Down
39 changes: 39 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ──────────────────────────────────────────────────────────────────────
Expand Down
3 changes: 2 additions & 1 deletion tests/test_level_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading