Repository: hikari-py/hikari
Pull request #1480 (2023-01-20 18:03:05)

Fix `ChannelFollowWebhook` not including source in all cases
### 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.


Patch:
diff --git a/changes/1480.bugfix.md b/changes/1480.bugfix.md
new file mode 100644
index 00000000..4226a0ca
--- /dev/null
+++ b/changes/1480.bugfix.md
@@ -0,0 +1,2 @@
+Fix `hikari.webhooks.ChannelFollowWebhook` not including source in all cases.
+- `source_channel` and `source_guild` will be `None` instead.
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 497b927f..aae4354c 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -3524,21 +3524,24 @@ def deserialize_channel_follower_webhook(
         self, payload: data_binding.JSONObject
     ) -> webhook_models.ChannelFollowerWebhook:
         application_id: typing.Optional[snowflakes.Snowflake] = None
-        if (raw_application_id := payload.get("application_id")) is not None:
+        if raw_application_id := payload.get("application_id"):
             application_id = snowflakes.Snowflake(raw_application_id)
 
-        raw_source_channel = payload["source_channel"]
-        # In this case the channel type isn't provided as we can safely
-        # assume it's a news channel.
-        raw_source_channel["type"] = channel_models.ChannelType.GUILD_NEWS
-        source_channel = self.deserialize_partial_channel(raw_source_channel)
-        source_guild_payload = payload["source_guild"]
-        source_guild = guild_models.PartialGuild(
-            app=self._app,
-            id=snowflakes.Snowflake(source_guild_payload["id"]),
-            name=source_guild_payload["name"],
-            icon_hash=source_guild_payload.get("icon"),
-        )
+        source_channel: typing.Optional[channel_models.PartialChannel] = None
+        if raw_source_channel := payload.get("source_channel"):
+            # In this case the channel type isn't provided as we can safely
+            # assume it's a news channel.
+            raw_source_channel.setdefault("type", channel_models.ChannelType.GUILD_NEWS)
+            source_channel = self.deserialize_partial_channel(raw_source_channel)
+
+        source_guild: typing.Optional[guild_models.PartialGuild] = None
+        if source_guild_payload := payload.get("source_guild"):
+            source_guild = guild_models.PartialGuild(
+                app=self._app,
+                id=snowflakes.Snowflake(source_guild_payload["id"]),
+                name=source_guild_payload["name"],
+                icon_hash=source_guild_payload.get("icon"),
+            )
 
         return webhook_models.ChannelFollowerWebhook(
             app=self._app,
diff --git a/hikari/webhooks.py b/hikari/webhooks.py
index 31d0605c..3f80903c 100644
--- a/hikari/webhooks.py
+++ b/hikari/webhooks.py
@@ -855,11 +855,19 @@ class ChannelFollowerWebhook(PartialWebhook):
         This will be `None` when received within an audit log.
     """
 
-    source_channel: channels_.PartialChannel = attr.field(eq=False, hash=False, repr=True)
-    """The partial object of the channel this webhook is following."""
+    source_channel: typing.Optional[channels_.PartialChannel] = attr.field(eq=False, hash=False, repr=True)
+    """The partial object of the channel this webhook is following.
 
-    source_guild: guilds_.PartialGuild = attr.field(eq=False, hash=False, repr=True)
-    """The partial object of the guild this webhook is following."""
+    This will be `None` when the user that followed the channel is no
+    longer in the source guild or has lost access to the source channel.
+    """
+
+    source_guild: typing.Optional[guilds_.PartialGuild] = attr.field(eq=False, hash=False, repr=True)
+    """The partial object of the guild this webhook is following.
+
+    This will be `None` when the user that followed the channel is no
+    longer in the source guild or has lost access to the source channel.
+    """
 
     async def delete(self) -> None:
         """Delete this webhook.


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 3929e750..bd90fd6b 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -6870,24 +6870,39 @@ def test_deserialize_channel_follower_webhook(
         assert webhook.source_guild.icon_hash == "bb71f469c158984e265093a81b3397fb"
         assert isinstance(webhook.source_guild, guild_models.PartialGuild)
 
-        assert webhook.source_channel == entity_factory_impl.deserialize_partial_channel(
-            {"id": "5618852344134324", "name": "announcements", "type": channel_models.ChannelType.GUILD_NEWS}
-        )
+        assert webhook.source_channel.id == 5618852344134324
+        assert webhook.source_channel.name == "announcements"
+        assert webhook.source_channel.type == channel_models.ChannelType.GUILD_NEWS
+        assert isinstance(webhook.source_channel, channel_models.PartialChannel)
+
         assert webhook.author == entity_factory_impl.deserialize_user(user_payload)
         assert isinstance(webhook, webhook_models.ChannelFollowerWebhook)
 
     def test_deserialize_channel_follower_webhook_without_optional_fields(
-        self, entity_factory_impl, mock_app, follower_webhook_payload, user_payload
+        self, entity_factory_impl, mock_app, follower_webhook_payload
     ):
         follower_webhook_payload["avatar"] = None
         del follower_webhook_payload["user"]
         del follower_webhook_payload["application_id"]
+        del follower_webhook_payload["source_guild"]
+        del follower_webhook_payload["source_channel"]
 
         webhook = entity_factory_impl.deserialize_channel_follower_webhook(follower_webhook_payload)
 
         assert webhook.avatar_hash is None
         assert webhook.application_id is None
         assert webhook.author is None
+        assert webhook.source_guild is None
+        assert webhook.source_channel is None
+
+    def test_deserialize_channel_follower_webhook_doesnt_set_source_channel_type_if_set(
+        self, entity_factory_impl, mock_app, follower_webhook_payload
+    ):
+        follower_webhook_payload["source_channel"]["type"] = channel_models.ChannelType.GUILD_VOICE
+
+        webhook = entity_factory_impl.deserialize_channel_follower_webhook(follower_webhook_payload)
+
+        assert webhook.source_channel.type == channel_models.ChannelType.GUILD_VOICE
 
     def test_deserialize_application_webhook(self, entity_factory_impl, mock_app, application_webhook_payload):
         webhook = entity_factory_impl.deserialize_application_webhook(application_webhook_payload)