Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add InteractionHandler class #64

Merged
merged 6 commits into from
Jan 4, 2024
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
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
name: "Pytest"
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
branches: master

jobs:
run_tests:
Expand Down
80 changes: 80 additions & 0 deletions suggestions/interaction_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from suggestions import SuggestionsBot


class InteractionHandler:
"""A generic interaction response class to allow for easier
testing and generification of interaction responses.

This class also aims to move the custom add-ons out of
the underlying disnake classes to help promote easier
version upgrading in the future.
"""

def __init__(
self, interaction: disnake.Interaction, ephemeral: bool, with_message: bool
):
self.interaction: disnake.Interaction = interaction
self.ephemeral: bool = ephemeral
self.with_message: bool = with_message
self.is_deferred: bool = False

# This is useful in error handling to stop
# getting discord "Interaction didn't respond"
# errors if we haven't yet sent anything
self.has_sent_something: bool = False

async def send(
self,
content: str | None = None,
*,
embed: disnake.Embed | None = None,
file: disnake.File | None = None,
components: list | None = None,
):
data = {}
if content is not None:
data["content"] = content
if embed is not None:
data["embed"] = embed
if file is not None:
data["file"] = file
if components is not None:
data["components"] = components

if not data:
raise ValueError("Expected at-least one value to send.")

await self.interaction.send(ephemeral=self.ephemeral, **data)
self.has_sent_something = True

@classmethod
async def new_handler(
cls,
interaction: disnake.Interaction,
bot: SuggestionsBot,
*,
ephemeral: bool = True,
with_message: bool = True,
) -> InteractionHandler:
"""Generate a new instance and defer the interaction."""
instance = cls(interaction, ephemeral, with_message)
await interaction.response.defer(ephemeral=ephemeral, with_message=with_message)
instance.is_deferred = True

# Register this on the bot instance so other areas can
# request the interaction handler, such as error handlers
bot.state.interaction_handlers.add_entry(interaction.application_id, instance)

return instance

@classmethod
async def fetch_handler(
cls, application_id: int, bot: SuggestionsBot
) -> InteractionHandler:
"""Fetch a registered handler for the given interaction."""
return bot.state.interaction_handlers.get_entry(application_id)
5 changes: 5 additions & 0 deletions suggestions/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from suggestions import SuggestionsBot
from alaric import Document
from suggestions.database import SuggestionsMongoManager
from suggestions.interaction_handler import InteractionHandler

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -59,6 +60,10 @@ def __init__(self, database: SuggestionsMongoManager, bot: SuggestionsBot):
self.existing_suggestion_ids: Set[str] = set()
self._background_tasks: list[asyncio.Task] = []

self.interaction_handlers: TimedCache[int, InteractionHandler] = TimedCache(
global_ttl=timedelta(minutes=20), lazy_eviction=False
)

@property
def is_closing(self) -> bool:
return self._is_closing
Expand Down
13 changes: 12 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import suggestions
from suggestions.clunk import ClunkLock, Clunk
from tests.mocks import MockedSuggestionsMongoManager
from suggestions.interaction_handler import InteractionHandler


@pytest.fixture
Expand All @@ -17,7 +18,7 @@ async def mocked_database() -> MockedSuggestionsMongoManager:


@pytest.fixture
async def causar(monkeypatch, mocked_database) -> Causar:
async def bot(monkeypatch):
if "./suggestions" not in [x[0] for x in os.walk(".")]:
monkeypatch.chdir("..")

Expand All @@ -35,6 +36,11 @@ async def causar(monkeypatch, mocked_database) -> Causar:

bot = await suggestions.create_bot(mocked_database)
await bot.load_cogs()
return bot


@pytest.fixture
async def causar(bot, mocked_database) -> Causar:
return Causar(bot) # type: ignore


Expand All @@ -53,3 +59,8 @@ async def clunk_lock(causar: Causar) -> ClunkLock:
@pytest.fixture
async def clunk(causar: Causar) -> Clunk:
return Clunk(causar.bot.state) # type: ignore


@pytest.fixture
def interaction_handler() -> InteractionHandler:
return InteractionHandler(AsyncMock(), True, True)
60 changes: 60 additions & 0 deletions tests/test_interaction_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from unittest.mock import AsyncMock

import pytest
from bot_base import NonExistentEntry

from suggestions.interaction_handler import InteractionHandler


async def test_send(interaction_handler: InteractionHandler):
assert interaction_handler.has_sent_something is False

with pytest.raises(ValueError):
await interaction_handler.send()

await interaction_handler.send("Hello world")
assert interaction_handler.has_sent_something is True
assert interaction_handler.interaction.send.assert_called_with(
content="Hello world", ephemeral=True
)

interaction_handler.interaction = AsyncMock()
await interaction_handler.send(
"Hello world", embed="Embed", file="File", components=["Test"]
)
assert interaction_handler.interaction.send.assert_called_with(
content="Hello world",
ephemeral=True,
embed="Embed",
file="File",
components=["Test"],
)


async def test_new_handler(bot):
assert bot.state.interaction_handlers.cache == {}
handler: InteractionHandler = await InteractionHandler.new_handler(AsyncMock(), bot)
assert bot.state.interaction_handlers.cache != {}
assert handler.has_sent_something is False
assert handler.is_deferred is True

handler_2: InteractionHandler = await InteractionHandler.new_handler(
AsyncMock(), bot, ephemeral=False, with_message=False
)
assert handler_2.ephemeral is False
assert handler_2.with_message is False


async def test_fetch_handler(bot):
application_id = 123456789
with pytest.raises(NonExistentEntry):
await InteractionHandler.fetch_handler(application_id, bot)

mock = AsyncMock()
mock.application_id = application_id
await InteractionHandler.new_handler(mock, bot, with_message=False)
handler: InteractionHandler = await InteractionHandler.fetch_handler(
application_id, bot
)
assert handler.with_message is False
assert handler.ephemeral is True
Loading