Repository: hikari-py/hikari (python)

Issue:
`Member.joined_at` is now nullable due to breaking API change
### Steps to reproduce

1. Invite guest via guest invite
2. Try deserializing the incoming events that contain the member (`VOICE_STATE_UPDATE` is definitely affected, unsure about the rest)
3. Bot explod


### Expected result

Bot not explod

### Actual result

Bot explod

```
TypeError: argument must be str
E 2024-01-20 01:05:35,693 asyncio: Exception occurred in raw event dispatch conduit
task: <Task pending name='dispatch VOICE_STATE_UPDATE' coro=<EventManagerBase._handle_dispatch() running at \hikari\impl\event_manager_base.py:609>>
Traceback (most recent call last):
  File "\hikari\impl\event_manager_base.py", line 602, in _handle_dispatch
    await consumer.callback(shard, payload)
  File "\hikari\impl\event_manager.py", line 810, in on_voice_state_update
    event = self._event_factory.deserialize_voice_state_update_event(shard, payload, old_state=old)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\hikari\impl\event_factory.py", line 919, in deserialize_voice_state_update_event
    state = self._app.entity_factory.deserialize_voice_state(payload)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\hikari\impl\entity_factory.py", line 3598, in deserialize_voice_state
    member = self.deserialize_member(payload["member"], guild_id=guild_id)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\hikari\impl\entity_factory.py", line 1809, in deserialize_member
    joined_at = time.iso8601_datetime_string_to_datetime(payload["joined_at"])
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

### Further info

The `joined_at` field is now nullable, according to this PR on the DAPI docs:

https://github.com/discord/discord-api-docs/pull/6247

This seems to only affect `VOICE_STATE_UPDATE` for now(?)

### Checklist

- [X] I have made sure to remove ANY sensitive information (bot token, passwords, credentials, personal details, etc.).
- [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 description:
`Member.joined_at` can now be None
Discord making breaking changes :D

### Related issues
Closes #1811


Patch:
diff --git a/changes/1812.bugfix.md b/changes/1812.bugfix.md
new file mode 100644
index 00000000..504d10e7
--- /dev/null
+++ b/changes/1812.bugfix.md
@@ -0,0 +1,2 @@
+`Member.joined_at` is now nullable due to breaking API change
+ - This will be received on guest members with temporary membership
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 599cab81..c570efe0 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -359,8 +359,12 @@ class Member(users.User):
     This will be `hikari.undefined.UNDEFINED` if it's state is unknown.
     """
 
-    joined_at: datetime.datetime = attrs.field(repr=True)
-    """The datetime of when this member joined the guild they belong to."""
+    joined_at: typing.Optional[datetime.datetime] = attrs.field(repr=True)
+    """The datetime of when this member joined the guild they belong to.
+
+    This will be `None` for guest members that have been temporarily
+    invited.
+    """
 
     nickname: typing.Optional[str] = attrs.field(repr=True)
     """This member's nickname.
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index a5f6cebf..507bdbb8 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -1806,7 +1806,8 @@ def deserialize_member(
         if guild_id not in role_ids:
             role_ids.append(guild_id)
 
-        joined_at = time.iso8601_datetime_string_to_datetime(payload["joined_at"])
+        raw_joined_at = payload["joined_at"]
+        joined_at = time.iso8601_datetime_string_to_datetime(raw_joined_at) if raw_joined_at is not None else None
 
         raw_premium_since = payload.get("premium_since")
         premium_since = (
diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py
index 89a5ba15..37551264 100644
--- a/hikari/internal/cache.py
+++ b/hikari/internal/cache.py
@@ -410,7 +410,7 @@ class MemberData(BaseData[guilds.Member]):
     nickname: typing.Optional[str] = attrs.field()
     guild_avatar_hash: typing.Optional[str] = attrs.field()
     role_ids: typing.Tuple[snowflakes.Snowflake, ...] = attrs.field()
-    joined_at: datetime.datetime = attrs.field()
+    joined_at: typing.Optional[datetime.datetime] = attrs.field()
     premium_since: typing.Optional[datetime.datetime] = attrs.field()
     is_deaf: undefined.UndefinedOr[bool] = attrs.field()
     is_mute: undefined.UndefinedOr[bool] = attrs.field()


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 35d07ed1..6c29210e 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -3230,7 +3230,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay
             {
                 "nick": None,
                 "roles": ["11111", "22222", "33333", "44444"],
-                "joined_at": "2015-04-26T06:26:56.936000+00:00",
+                "joined_at": None,
                 "premium_since": None,
                 "deaf": False,
                 "avatar": None,
@@ -3244,6 +3244,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay
         assert member.nickname is None
         assert member.premium_since is None
         assert member.guild_avatar_hash is None
+        assert member.joined_at is None
         assert isinstance(member, guild_models.Member)
 
     def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, user_payload):