Repository: hikari-py/hikari (python)

Issue:
Specialize InteractionCreateEvent
### Summary

Specialize `InteractionCreateEvent` into the different interaction types, similar how `MessageCreateEvent` is specialized for guilds and dms

### Why is this needed?

Nicer DX

### Ideal implementation

Provide more concrete subclasses with correct typing for `interaction`. 

### Checklist

- [X] I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.

Pull request:
Specialize Interaction Events
### Summary
As described here #1777 

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
Closes #1777 


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index acd26b4f..ca28727b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,9 +22,9 @@ jobs:
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
         with:
           languages: python
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
diff --git a/changes/2241.feature.md b/changes/2241.feature.md
new file mode 100644
index 00000000..78c05163
--- /dev/null
+++ b/changes/2241.feature.md
@@ -0,0 +1,1 @@
+Added more specialized interaction events `CommandInteractionCreateEvent`, `ComponentInteractionCreateEvent`, `AutocompleteInteractionCreateEvent` and `ModalInteractionCreateEvent` to improve developer experience
diff --git a/examples/slash.py b/examples/slash.py
index 2803e5fd..9dadf58e 100644
--- a/examples/slash.py
+++ b/examples/slash.py
@@ -40,12 +40,8 @@ async def register_commands(event: hikari.StartingEvent) -> None:
 
 
 @bot.listen()
-async def handle_interactions(event: hikari.InteractionCreateEvent) -> None:
+async def handle_interactions(event: hikari.CommandInteractionCreateEvent) -> None:
     """Listen for slash commands being executed."""
-    if not isinstance(event.interaction, hikari.CommandInteraction):
-        # only listen to command interactions, no others!
-        return
-
     if event.interaction.command_name == "ping":
         await event.interaction.create_initial_response(
             hikari.ResponseType.MESSAGE_CREATE, f"Pong! {bot.heartbeat_latency * 1_000:.0f}ms"
diff --git a/hikari/events/interaction_events.py b/hikari/events/interaction_events.py
index 5f16e1e4..52a84fb1 100644
--- a/hikari/events/interaction_events.py
+++ b/hikari/events/interaction_events.py
@@ -23,7 +23,13 @@
 
 from __future__ import annotations
 
-__all__: typing.Sequence[str] = ("InteractionCreateEvent",)
+__all__: typing.Sequence[str] = (
+    "InteractionCreateEvent",
+    "CommandInteractionCreateEvent",
+    "ComponentInteractionCreateEvent",
+    "AutocompleteInteractionCreateEvent",
+    "ModalInteractionCreateEvent",
+)
 
 import typing
 
@@ -36,6 +42,9 @@
     from hikari import traits
     from hikari.api import shard as gateway_shard
     from hikari.interactions import base_interactions
+    from hikari.interactions import command_interactions
+    from hikari.interactions import component_interactions
+    from hikari.interactions import modal_interactions
 
 
 @attrs_extensions.with_copy
@@ -53,3 +62,39 @@ class InteractionCreateEvent(shard_events.ShardEvent):
     def app(self) -> traits.RESTAware:
         # <<inherited docstring from Event>>.
         return self.interaction.app
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class CommandInteractionCreateEvent(InteractionCreateEvent):
+    """Event fired when a command interaction is created."""
+
+    interaction: command_interactions.CommandInteraction = attrs.field(repr=True)
+    """Interaction that this event is related to."""
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class ComponentInteractionCreateEvent(InteractionCreateEvent):
+    """Event fired when a component interaction is created."""
+
+    interaction: component_interactions.ComponentInteraction = attrs.field(repr=True)
+    """Interaction that this event is related to."""
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class AutocompleteInteractionCreateEvent(InteractionCreateEvent):
+    """Event fired when an autocomplete interaction is created."""
+
+    interaction: command_interactions.AutocompleteInteraction = attrs.field(repr=True)
+    """Interaction that this event is related to."""
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class ModalInteractionCreateEvent(InteractionCreateEvent):
+    """Event fired when a modal interaction is created."""
+
+    interaction: modal_interactions.ModalInteraction = attrs.field(repr=True)
+    """Interaction that this event is related to."""
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index 6fc68f73..df1c4f9d 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -53,6 +53,7 @@
 from hikari.events import typing_events
 from hikari.events import user_events
 from hikari.events import voice_events
+from hikari.interactions import base_interactions
 from hikari.internal import collections
 from hikari.internal import data_binding
 from hikari.internal import time
@@ -67,6 +68,15 @@
     from hikari import voices as voices_models
     from hikari.api import shard as gateway_shard
 
+_INTERACTION_EVENTS_MAP: dict[
+    base_interactions.InteractionType, typing.Type[interaction_events.InteractionCreateEvent]
+] = {
+    base_interactions.InteractionType.APPLICATION_COMMAND: interaction_events.CommandInteractionCreateEvent,
+    base_interactions.InteractionType.AUTOCOMPLETE: interaction_events.AutocompleteInteractionCreateEvent,
+    base_interactions.InteractionType.MESSAGE_COMPONENT: interaction_events.ComponentInteractionCreateEvent,
+    base_interactions.InteractionType.MODAL_SUBMIT: interaction_events.ModalInteractionCreateEvent,
+}
+
 
 class EventFactoryImpl(event_factory.EventFactory):
     """Implementation for a single-application bot event factory."""
@@ -493,9 +503,9 @@ def deserialize_audit_log_entry_create_event(
     def deserialize_interaction_create_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
     ) -> interaction_events.InteractionCreateEvent:
-        return interaction_events.InteractionCreateEvent(
-            shard=shard, interaction=self._app.entity_factory.deserialize_interaction(payload)
-        )
+        interaction = self._app.entity_factory.deserialize_interaction(payload)
+
+        return _INTERACTION_EVENTS_MAP[interaction.type](shard=shard, interaction=interaction)
 
     #################
     # MEMBER EVENTS #
diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py
index 8f2d9a23..7a2695a0 100644
--- a/hikari/interactions/base_interactions.py
+++ b/hikari/interactions/base_interactions.py
@@ -214,7 +214,7 @@ class PartialInteraction(snowflakes.Unique, webhooks.ExecutableWebhook):
     application_id: snowflakes.Snowflake = attrs.field(eq=False, repr=False)
     """ID of the application this interaction belongs to."""
 
-    type: typing.Union[InteractionType, int] = attrs.field(eq=False, repr=True)
+    type: InteractionType = attrs.field(eq=False, repr=True)
     """The type of interaction this is."""
 
     token: str = attrs.field(eq=False, repr=False)


Test patch:
diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py
index e7ef4cf3..150b4a94 100644
--- a/tests/hikari/impl/test_event_factory.py
+++ b/tests/hikari/impl/test_event_factory.py
@@ -21,6 +21,7 @@
 from __future__ import annotations
 
 import datetime
+import typing
 
 import mock
 import pytest
@@ -48,6 +49,7 @@
 from hikari.events import user_events
 from hikari.events import voice_events
 from hikari.impl import event_factory as event_factory_
+from hikari.interactions import base_interactions
 
 
 class TestEventFactoryImpl:
@@ -733,15 +735,37 @@ def test_deserialize_audit_log_entry_create_event(
     # INTERACTION EVENTS #
     ######################
 
-    def test_deserialize_interaction_create_event(self, event_factory, mock_app, mock_shard):
+    @pytest.mark.parametrize(
+        ("interaction_type", "expected"),
+        [
+            (base_interactions.InteractionType.APPLICATION_COMMAND, interaction_events.CommandInteractionCreateEvent),
+            (base_interactions.InteractionType.MESSAGE_COMPONENT, interaction_events.ComponentInteractionCreateEvent),
+            (base_interactions.InteractionType.AUTOCOMPLETE, interaction_events.AutocompleteInteractionCreateEvent),
+            (base_interactions.InteractionType.MODAL_SUBMIT, interaction_events.ModalInteractionCreateEvent),
+        ],
+    )
+    def test_deserialize_interaction_create_event(
+        self,
+        event_factory,
+        mock_app,
+        mock_shard,
+        interaction_type: typing.Optional[base_interactions.InteractionType],
+        expected: interaction_events.InteractionCreateEvent,
+    ):
         payload = {"id": "1561232344"}
-
+        if interaction_type:
+            mock_app.entity_factory.deserialize_interaction.return_value = mock.Mock(type=interaction_type)
         result = event_factory.deserialize_interaction_create_event(mock_shard, payload)
 
         mock_app.entity_factory.deserialize_interaction.assert_called_once_with(payload)
         assert result.shard is mock_shard
         assert result.interaction is mock_app.entity_factory.deserialize_interaction.return_value
-        assert isinstance(result, interaction_events.InteractionCreateEvent)
+        assert isinstance(result, expected)
+
+    def test_deserialize_interaction_create_event_error(self, event_factory, mock_app, mock_shard):
+        payload = {"id": "1561232344"}
+        with pytest.raises(KeyError):
+            event_factory.deserialize_interaction_create_event(mock_shard, payload)
 
     #################
     # MEMBER EVENTS #