Repository: Rapptz/discord.py

Issue:
[v2]: DM typing and Thread typing events are not dispatched.
### Summary

DM typing events are not dispatched.

### Reproduction Steps

Start typing in a dm or a thread with an on_typing listener. The on_typing listener will never be dispatched.

### Minimal Reproducible Code

```python
import discord
import logging

logging.getLogger("discord").setLevel(logging.DEBUG)


class MyClient(discord.Client):
    async def on_ready(self):
        print(f"Logged in as {self.user} (ID: {self.user.id})")
        print("------")

    async def on_typing(self, channel, user, _):
        # we do not want the bot to reply to itself
        if user.id == self.user.id:
            return

        # these never gets called in a DM channel or Thread.
        print(f"{user} is typing in {channel}")
        await channel.trigger_typing()

    async def on_socket_raw_receive(self, msg):
        print(msg)


client = MyClient(intents=discord.Intents(typing=True), enable_debug_events=True)
client.run("token")
```


### Expected Results

The on_typing event should be dispatched to the client for dm typing events.

### Actual Results

The on_typing event is not dispatched to the client for dm typing events.

### Intents

typing=True ; the issue occurs even with all intents

### System Information

- Python v3.8.10-final
- discord.py v2.0.0-alpha
    - discord.py pkg_resources: v2.0.0a3495+gb6590d7f
- aiohttp v3.7.4.post0
- system info: Linux 5.10.43.3-microsoft-standard-WSL2 #1 SMP Wed Jun 16 23:47:55 UTC 2021

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

https://canary.discord.com/channels/815369174096412692/815369174096412695/878394332640591922


Discussion:
Partially fixed in fbc4a51c355c029af61363038d0a8a9ac0bd8ace, dm typing is still not sent.
DM typings cannot be sent at the moment since the data is incomplete. The event only gives `user_id` so there's no good way to resolve it to a full `User` object. This might be impossible to fix without an event refactor for raw events.
Hmmmm, what about a middle solution, of a raw_typing event? This wouldn't need to be resolved to a full user object, and other raw_ events already exist, so it wouldn't be unusual. All that it would provide are the user id and the channel id.

Resolving patch:
diff --git a/discord/raw_models.py b/discord/raw_models.py
--- a/discord/raw_models.py
+++ b/discord/raw_models.py
@@ -24,9 +24,11 @@
 
 from __future__ import annotations
 
+import datetime
 from typing import TYPE_CHECKING, Optional, Set, List, Tuple, Union
 
 from .enums import ChannelType, try_enum
+from .utils import _get_as_snowflake
 
 if TYPE_CHECKING:
     from .types.gateway import (
@@ -39,11 +41,13 @@
         MessageUpdateEvent,
         IntegrationDeleteEvent,
         ThreadDeleteEvent,
+        TypingStartEvent,
     )
     from .message import Message
     from .partial_emoji import PartialEmoji
     from .member import Member
     from .threads import Thread
+    from .user import User
 
     ReactionActionEvent = Union[MessageReactionAddEvent, MessageReactionRemoveEvent]
 
@@ -57,6 +61,7 @@
     'RawReactionClearEmojiEvent',
     'RawIntegrationDeleteEvent',
     'RawThreadDeleteEvent',
+    'RawTypingEvent',
 )
 
 
@@ -314,3 +319,32 @@ def __init__(self, data: ThreadDeleteEvent) -> None:
         self.guild_id: int = int(data['guild_id'])
         self.parent_id: int = int(data['parent_id'])
         self.thread: Optional[Thread] = None
+
+
+class RawTypingEvent(_RawReprMixin):
+    """Represents the payload for a :func:`on_raw_typing` event.
+
+    .. versionadded:: 2.0
+
+    Attributes
+    ----------
+    channel_id: :class:`int`
+        The ID of the channel the user started typing in.
+    user_id: :class:`int`
+        The ID of the user that started typing.
+    user: Optional[Union[:class:`discord.User`, :class:`discord.Member`]]
+        The user that started typing, if they could be found in the internal cache.
+    timestamp: :class:`datetime.datetime`
+        When the typing started as an aware datetime in UTC.
+    guild_id: Optional[:class:`int`]
+        The ID of the guild the user started typing in, if applicable.
+    """
+
+    __slots__ = ('channel_id', 'user_id', 'user', 'timestamp', 'guild_id')
+
+    def __init__(self, data: TypingStartEvent, /) -> None:
+        self.channel_id: int = int(data['channel_id'])
+        self.user_id: int = int(data['user_id'])
+        self.user: Optional[Union[User, Member]] = None
+        self.timestamp: datetime.datetime = datetime.datetime.fromtimestamp(data['timestamp'], tz=datetime.timezone.utc)
+        self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
diff --git a/discord/state.py b/discord/state.py
--- a/discord/state.py
+++ b/discord/state.py
@@ -27,7 +27,6 @@
 import asyncio
 from collections import deque, OrderedDict
 import copy
-import datetime
 import itertools
 import logging
 from typing import (
@@ -1443,27 +1442,25 @@ def parse_voice_server_update(self, data: gw.VoiceServerUpdateEvent) -> None:
             asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
 
     def parse_typing_start(self, data: gw.TypingStartEvent) -> None:
+        raw = RawTypingEvent(data)
+        raw.user = self.get_user(raw.user_id)
         channel, guild = self._get_guild_channel(data)
+
         if channel is not None:
-            member = None
-            user_id = int(data['user_id'])
             if isinstance(channel, DMChannel):
-                member = channel.recipient
-
-            elif isinstance(channel, (Thread, TextChannel)) and guild is not None:
-                member = guild.get_member(user_id)
+                channel.recipient = raw.user
+            elif guild is not None:
+                raw.user = guild.get_member(raw.user_id)
 
-                if member is None:
+                if raw.user is None:
                     member_data = data.get('member')
                     if member_data:
-                        member = Member(data=member_data, state=self, guild=guild)
+                        raw.user = Member(data=member_data, state=self, guild=guild)
 
-            elif isinstance(channel, GroupChannel):
-                member = utils.find(lambda x: x.id == user_id, channel.recipients)
+            if raw.user is not None:
+                self.dispatch('typing', channel, raw.user, raw.timestamp)
 
-            if member is not None:
-                timestamp = datetime.datetime.fromtimestamp(data['timestamp'], tz=datetime.timezone.utc)
-                self.dispatch('typing', channel, member, timestamp)
+        self.dispatch('raw_typing', raw)
 
     def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
         if isinstance(channel, TextChannel):


Test patch: