Repository: hikari-py/hikari
Pull request #338 (2020-11-02 19:37:17)

Add new integration events
### Summary
Add new integration events to replace guild integrations update

### 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
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->


davfsa: Feel free to merge when you feel it's done

Patch:
diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py
index 40a332cd..05bb4f0b 100644
--- a/hikari/api/entity_factory.py
+++ b/hikari/api/entity_factory.py
@@ -714,7 +714,12 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g
         """
 
     @abc.abstractmethod
-    def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_models.Integration:
+    def deserialize_integration(
+        self,
+        payload: data_binding.JSONObject,
+        *,
+        guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
+    ) -> guild_models.Integration:
         """Parse a raw payload from Discord into an integration object.
 
         Parameters
@@ -722,10 +727,23 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_mod
         payload : hikari.internal.data_binding.JSONObject
             The JSON payload to deserialize.
 
+        Other Parameters
+        ----------------
+        guild_id : hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake]
+            The ID of the guild this integration belongs to. If this is specified
+            then this will be prioritised over `"guild_id"` in the payload.
+
         Returns
         -------
         hikari.guilds.Integration
             The deserialized integration object.
+
+        Raises
+        ------
+        KeyError
+            If `guild_id` is left as `hikari.undefined.UNDEFINED` when
+            `"guild_id"` is not present in the passed payload for the payload of
+            the integration.
         """
 
     @abc.abstractmethod
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index c256a9a6..2362ca76 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -325,10 +325,10 @@ def deserialize_guild_emojis_update_event(
             The parsed guild emojis update event object.
         """
 
-    def deserialize_guild_integrations_update_event(
+    def deserialize_integration_create_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> guild_events.IntegrationsUpdateEvent:
-        """Parse a raw payload from Discord into a guilds integrations update event object.
+    ) -> guild_events.IntegrationCreateEvent:
+        """Parse a raw payload from Discord into an integration create event object.
 
         Parameters
         ----------
@@ -339,8 +339,44 @@ def deserialize_guild_integrations_update_event(
 
         Returns
         -------
-        hikari.events.guild_events.IntegrationsUpdateEvent
-            The parsed guilds integrations update event object.
+        hikari.events.guild_events.IntegrationCreateEvent
+            The parsed integration create event object.
+        """
+
+    def deserialize_integration_delete_event(
+        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+    ) -> guild_events.IntegrationDeleteEvent:
+        """Parse a raw payload from Discord into an integration delete event object.
+
+        Parameters
+        ----------
+        shard : hikari.api.shard.GatewayShard
+            The shard that emitted this event.
+        payload : hikari.internal.data_binding.JSONObject
+            The dict payload to parse.
+
+        Returns
+        -------
+        hikari.events.guild_events.IntegrationDeleteEvent
+            The parsed integration delete event object.
+        """
+
+    def deserialize_integration_update_event(
+        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+    ) -> guild_events.IntegrationUpdateEvent:
+        """Parse a raw payload from Discord into an integration update event object.
+
+        Parameters
+        ----------
+        shard : hikari.api.shard.GatewayShard
+            The shard that emitted this event.
+        payload : hikari.internal.data_binding.JSONObject
+            The dict payload to parse.
+
+        Returns
+        -------
+        hikari.events.guild_events.IntegrationUpdateEvent
+            The parsed integration update event object.
         """
 
     def deserialize_guild_member_add_event(
diff --git a/hikari/applications.py b/hikari/applications.py
index a0b9f0da..f21c529e 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -365,26 +365,12 @@ def format_icon(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[
 
 @attr_extensions.with_copy
 @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
-class Application(snowflakes.Unique):
+class Application(guilds.PartialApplication):
     """Represents the information of an Oauth2 Application."""
 
     app: traits.RESTAware = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True})
     """The client application that models may use for procedures."""
 
-    id: snowflakes.Snowflake = attr.ib(
-        eq=True,
-        hash=True,
-        repr=True,
-    )
-    """The ID of this entity."""
-
-    name: str = attr.ib(eq=False, hash=False, repr=True)
-    """The name of this application."""
-
-    # TODO: default to None for consistency?
-    description: str = attr.ib(eq=False, hash=False, repr=False)
-    """The description of this application, or an empty string if undefined."""
-
     is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True)
     """`builtins.True` if the bot associated with this application is public.
 
@@ -407,18 +393,9 @@ class Application(snowflakes.Unique):
     rpc_origins: typing.Optional[typing.Sequence[str]] = attr.ib(eq=False, hash=False, repr=False)
     """A collection of this application's RPC origin URLs, if RPC is enabled."""
 
-    summary: str = attr.ib(eq=False, hash=False, repr=False)
-    """This summary for this application's primary SKU if it's sold on Discord.
-
-    Will be an empty string if undefined.
-    """
-
     verify_key: typing.Optional[bytes] = attr.ib(eq=False, hash=False, repr=False)
     """The base64 encoded key used for the GameSDK's `GetTicket`."""
 
-    icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
-    """The CDN hash of this application's icon, if set."""
-
     team: typing.Optional[Team] = attr.ib(eq=False, hash=False, repr=False)
     """The team this application belongs to.
 
@@ -440,54 +417,6 @@ class Application(snowflakes.Unique):
     cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
     """The CDN's hash of this application's cover image, used on the store."""
 
-    def __str__(self) -> str:
-        return self.name
-
-    @property
-    def icon_url(self) -> typing.Optional[files.URL]:
-        """Team icon, if there is one.
-
-        Returns
-        -------
-        typing.Optional[hikari.files.URL]
-            The URL, or `builtins.None` if no icon exists.
-        """
-        return self.format_icon()
-
-    def format_icon(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
-        """Generate the icon for this application.
-
-        Parameters
-        ----------
-        ext : builtins.str
-            The extension to use for this URL, defaults to `png`.
-            Supports `png`, `jpeg`, `jpg` and `webp`.
-        size : builtins.int
-            The size to set for the URL, defaults to `4096`.
-            Can be any power of two between 16 and 4096.
-
-        Returns
-        -------
-        typing.Optional[hikari.files.URL]
-            The URL, or `builtins.None` if no icon exists.
-
-        Raises
-        ------
-        builtins.ValueError
-            If the size is not an integer power of 2 between 16 and 4096
-            (inclusive).
-        """
-        if self.icon_hash is None:
-            return None
-
-        return routes.CDN_APPLICATION_ICON.compile_to_file(
-            urls.CDN_URL,
-            application_id=self.id,
-            hash=self.icon_hash,
-            size=size,
-            file_format=ext,
-        )
-
     @property
     def cover_image_url(self) -> typing.Optional[files.URL]:
         """Cover image used on the store.
diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py
index 4523af39..6bba5864 100644
--- a/hikari/events/guild_events.py
+++ b/hikari/events/guild_events.py
@@ -34,7 +34,10 @@
     "BanCreateEvent",
     "BanDeleteEvent",
     "EmojisUpdateEvent",
-    "IntegrationsUpdateEvent",
+    "IntegrationEvent",
+    "IntegrationCreateEvent",
+    "IntegrationDeleteEvent",
+    "IntegrationUpdateEvent",
     "PresenceUpdateEvent",
 ]
 
@@ -410,33 +413,32 @@ async def fetch_emojis(self) -> typing.Sequence[emojis_.KnownCustomEmoji]:
         return await self.app.rest.fetch_guild_emojis(self.guild_id)
 
 
-@attr_extensions.with_copy
 @attr.s(kw_only=True, slots=True, weakref_slot=False)
-@base_events.requires_intents(intents.Intents.GUILD_EMOJIS)
-class IntegrationsUpdateEvent(GuildEvent):
-    """Event that is fired when the integrations in a guild are changed.
-
-    This may occur when integrations are created, updated, or deleted.
-
-    !!! note
-        This event is similar to
-        `hikari.events.channel_events.WebhookUpdateEvent` in that Discord
-        does not provide any information on what was actually changed, nor
-        how it was changed. The only way you will be able to determine this is
-        to keep a cache of this information manually up to date by fetching
-        it using REST API calls. This is a limitation of Discord's design.
-        We agree that it is not overly helpful to you.
-    """
+@base_events.requires_intents(intents.Intents.GUILD_INTEGRATIONS)
+class IntegrationEvent(GuildEvent, abc.ABC):
+    """Event base for any integration related events."""
 
-    app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
-    # <<inherited docstring from Event>>.
+    @property
+    @abc.abstractmethod
+    def application_id(self) -> typing.Optional[snowflakes.Snowflake]:
+        """ID of Discord bot application this integration is connected to.
 
-    shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
-    # <<inherited docstring from ShardEvent>>.
+        Returns
+        -------
+        typing.Optional[hikari.snowflakes.Snowflake]
+            The ID of Discord bot application this integration is connected to.
+        """
 
-    guild_id: snowflakes.Snowflake = attr.ib()
+    @property
+    @abc.abstractmethod
+    def id(self) -> snowflakes.Snowflake:
+        """ID of the integration.
 
-    # <<inherited docstring from ShardEvent>>.
+        Returns
+        -------
+        hikari.snowflakes.Snowflake
+            The ID of the integration.
+        """
 
     async def fetch_integrations(self) -> typing.Sequence[guilds.Integration]:
         """Perform an API call to fetch some number of guild integrations.
@@ -457,6 +459,90 @@ async def fetch_integrations(self) -> typing.Sequence[guilds.Integration]:
         return await self.app.rest.fetch_integrations(self.guild_id)
 
 
+@attr_extensions.with_copy
+@attr.s(kw_only=True, slots=True, weakref_slot=False)
+@base_events.requires_intents(intents.Intents.GUILD_INTEGRATIONS)
+class IntegrationCreateEvent(IntegrationEvent):
+    """Event that is fired when an integration is created in a guild."""
+
+    app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from Event>>.
+
+    shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from ShardEvent>>.
+
+    integration: guilds.Integration = attr.ib()
+    """Integration that was created."""
+
+    @property
+    def application_id(self) -> typing.Optional[snowflakes.Snowflake]:
+        # <<inherited docstring from IntegrationEvent>>.
+        return self.integration.application.id if self.integration.application else None
+
+    @property
+    def guild_id(self) -> snowflakes.Snowflake:
+        # <<inherited docstring from ShardEvent>>.
+        return self.integration.guild_id
+
+    @property
+    def id(self) -> snowflakes.Snowflake:
+        # <<inherited docstring from IntegrationEvent>>
+        return self.integration.id
+
+
+@attr_extensions.with_copy
+@attr.s(kw_only=True, slots=True, weakref_slot=False)
+@base_events.requires_intents(intents.Intents.GUILD_INTEGRATIONS)
+class IntegrationDeleteEvent(IntegrationEvent):
+    """Event that is fired when an integration is deleted in a guild."""
+
+    app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from Event>>.
+
+    shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from ShardEvent>>.
+
+    application_id: typing.Optional[snowflakes.Snowflake] = attr.ib()
+    # <<inherited docstring from IntegrationEvent>>.
+
+    guild_id: snowflakes.Snowflake = attr.ib()
+    # <<inherited docstring from ShardEvent>>.
+
+    id: snowflakes.Snowflake = attr.ib()
+    # <<inherited docstring from IntegrationEvent>>
+
+
+@attr_extensions.with_copy
+@attr.s(kw_only=True, slots=True, weakref_slot=False)
+@base_events.requires_intents(intents.Intents.GUILD_INTEGRATIONS)
+class IntegrationUpdateEvent(IntegrationEvent):
+    """Event that is fired when an integration is updated in a guild."""
+
+    app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from Event>>.
+
+    shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from ShardEvent>>.
+
+    integration: guilds.Integration = attr.ib()
+    """Integration that was updated."""
+
+    @property
+    def application_id(self) -> typing.Optional[snowflakes.Snowflake]:
+        # <<inherited docstring from IntegrationEvent>>.
+        return self.integration.application.id if self.integration.application else None
+
+    @property
+    def guild_id(self) -> snowflakes.Snowflake:
+        # <<inherited docstring from GuildEvent>>.
+        return self.integration.guild_id
+
+    @property
+    def id(self) -> snowflakes.Snowflake:
+        # <<inherited docstring from IntegrationEvent>>
+        return self.integration.id
+
+
 @attr_extensions.with_copy
 @attr.s(kw_only=True, slots=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_PRESENCES)
diff --git a/hikari/guilds.py b/hikari/guilds.py
index c9927b1f..5b555ef4 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -547,68 +547,91 @@ def __str__(self) -> str:
         return self.name
 
 
+# This is here rather than in applications.py to avoid circular imports
 @attr_extensions.with_copy
 @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
-class IntegrationApplication:
-    """An application that's linked to an integration."""
+class PartialApplication(snowflakes.Unique):
+    """A partial representation of a Discord application."""
 
-    id: snowflakes.Snowflake = attr.ib(eq=True, hash=True, repr=True)
+    id: snowflakes.Snowflake = attr.ib(
+        eq=True,
+        hash=True,
+        repr=True,
+    )
     """The ID of this entity."""
 
     name: str = attr.ib(eq=False, hash=False, repr=True)
     """The name of this application."""
 
+    # TODO: default to None for consistency?
+    description: str = attr.ib(eq=False, hash=False, repr=False)
+    """The description of this application, or an empty string if undefined."""
+
     icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
-    """The icon hash of this application."""
+    """The CDN hash of this application's icon, if set."""
 
     summary: str = attr.ib(eq=False, hash=False, repr=False)
-    """The summary of this application."""
+    """This summary for this application's primary SKU if it's sold on Discord.
 
-    description: str = attr.ib(eq=False, hash=False, repr=False)
-    """The description of this application."""
+    Will be an empty string if undefined.
+    """
 
-    bot: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=False)
-    """The bot associated with this application."""
+    def __str__(self) -> str:
+        return self.name
+
+    @property
+    def icon_url(self) -> typing.Optional[files.URL]:
+        """Team icon, if there is one.
+
+        Returns
+        -------
+        typing.Optional[hikari.files.URL]
+            The URL, or `builtins.None` if no icon exists.
+        """
+        return self.format_icon()
 
-    def format_avatar(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
-        """Generate the avatar for this application, if set.
+    def format_icon(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
+        """Generate the icon for this application.
 
         Parameters
         ----------
-        ext : typing.Optional[builtins.str]
-            The ext to use for this URL, defaults to `png` or `gif`.
-            Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when
-            animated).
-
-            Defaults to `png`.
+        ext : builtins.str
+            The extension to use for this URL, defaults to `png`.
+            Supports `png`, `jpeg`, `jpg` and `webp`.
         size : builtins.int
             The size to set for the URL, defaults to `4096`.
             Can be any power of two between 16 and 4096.
-            Will be ignored for default avatars.
 
         Returns
         -------
         typing.Optional[hikari.files.URL]
-            The URL to the avatar, or `builtins.None` if not present.
+            The URL, or `builtins.None` if no icon exists.
 
         Raises
         ------
         builtins.ValueError
-            If `size` is not a power of two or not between 16 and 4096.
+            If the size is not an integer power of 2 between 16 and 4096
+            (inclusive).
         """
         if self.icon_hash is None:
             return None
 
         return routes.CDN_APPLICATION_ICON.compile_to_file(
             urls.CDN_URL,
-            user_id=self.id,
+            application_id=self.id,
             hash=self.icon_hash,
             size=size,
             file_format=ext,
         )
 
-    def __str__(self) -> str:
-        return self.name
+
+@attr_extensions.with_copy
+@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
+class IntegrationApplication(PartialApplication):
+    """An application that's linked to an integration."""
+
+    bot: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=False)
+    """The bot associated with this application."""
 
 
 @attr_extensions.with_copy
@@ -636,6 +659,9 @@ def __str__(self) -> str:
 class Integration(PartialIntegration):
     """Represents a guild integration object."""
 
+    guild_id: snowflakes.Snowflake = attr.ib()
+    """The ID of the guild this integration belongs to."""
+
     expire_behavior: typing.Union[IntegrationExpireBehaviour, int, None] = attr.ib(eq=False, hash=False, repr=False)
     """How members should be treated after their connected subscription expires.
 
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 73e27f69..bdfddc2a 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -1053,7 +1053,12 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g
             account=integration_fields.account,
         )
 
-    def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_models.Integration:
+    def deserialize_integration(
+        self,
+        payload: data_binding.JSONObject,
+        *,
+        guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
+    ) -> guild_models.Integration:
         integration_fields = self._set_partial_integration_attributes(payload)
 
         role_id: typing.Optional[snowflakes.Snowflake] = None
@@ -1093,6 +1098,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_mod
 
         return guild_models.Integration(
             id=integration_fields.id,
+            guild_id=guild_id if guild_id is not undefined.UNDEFINED else snowflakes.Snowflake(payload["guild_id"]),
             name=integration_fields.name,
             type=integration_fields.type,
             account=integration_fields.account,
@@ -1519,9 +1525,22 @@ def deserialize_partial_message(self, payload: data_binding.JSONObject) -> messa
                 party_id=activity_payload.get("party_id"),
             )
 
-        application: undefined.UndefinedOr[application_models.Application] = undefined.UNDEFINED
+        application: undefined.UndefinedOr[message_models.MessageApplication] = undefined.UNDEFINED
         if "application" in payload:
-            application = self.deserialize_application(payload["application"])
+            application_payload = payload["application"]
+            primary_sku_id: typing.Optional[snowflakes.Snowflake] = None
+            if (raw_primary_sku_id := application_payload.get("primary_sku_id")) is not None:
+                primary_sku_id = snowflakes.Snowflake(raw_primary_sku_id)
+
+            application = message_models.MessageApplication(
+                id=snowflakes.Snowflake(application_payload["id"]),
+                name=application_payload["name"],
+                description=application_payload["description"],
+                icon_hash=application_payload["icon"],
+                summary=application_payload["summary"],
+                cover_image_hash=application_payload["cover_image"],
+                primary_sku_id=primary_sku_id,
+            )
 
         message_reference: undefined.UndefinedOr[message_models.MessageCrosspost] = undefined.UNDEFINED
         if "message_reference" in payload:
@@ -1632,6 +1651,23 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model
                 else None,
             )
 
+        application: typing.Optional[message_models.MessageApplication] = None
+        if "application" in payload:
+            application_payload = payload["application"]
+            primary_sku_id: typing.Optional[snowflakes.Snowflake] = None
+            if (raw_primary_sku_id := application_payload.get("primary_sku_id")) is not None:
+                primary_sku_id = snowflakes.Snowflake(raw_primary_sku_id)
+
+            application = message_models.MessageApplication(
+                id=snowflakes.Snowflake(application_payload["id"]),
+                name=application_payload["name"],
+                description=application_payload["description"],
+                icon_hash=application_payload["icon"],
+                summary=application_payload["summary"],
+                cover_image_hash=application_payload["cover_image"],
+                primary_sku_id=primary_sku_id,
+            )
+
         return message_models.Message(
             app=self._app,
             id=snowflakes.Snowflake(payload["id"]),
@@ -1654,7 +1690,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model
             webhook_id=snowflakes.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None,
             type=message_models.MessageType(payload["type"]),
             activity=activity,
-            application=self.deserialize_application(payload["application"]) if "application" in payload else None,
+            application=application,
             message_reference=crosspost,
             flags=message_models.MessageFlag(payload["flags"]) if "flags" in payload else None,
             nonce=payload.get("nonce"),
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index cad471fc..300f803f 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -249,13 +249,33 @@ def deserialize_guild_emojis_update_event(
         ]
         return guild_events.EmojisUpdateEvent(app=self._app, shard=shard, guild_id=guild_id, emojis=emojis)
 
-    def deserialize_guild_integrations_update_event(
+    def deserialize_integration_create_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> guild_events.IntegrationsUpdateEvent:
-        return guild_events.IntegrationsUpdateEvent(
+    ) -> guild_events.IntegrationCreateEvent:
+        return guild_events.IntegrationCreateEvent(
+            app=self._app, shard=shard, integration=self._app.entity_factory.deserialize_integration(payload)
+        )
+
+    def deserialize_integration_delete_event(
+        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+    ) -> guild_events.IntegrationDeleteEvent:
+        application_id: typing.Optional[snowflakes.Snowflake] = None
+        if (raw_application_id := payload.get("application_id")) is not None:
+            application_id = snowflakes.Snowflake(raw_application_id)
+
+        return guild_events.IntegrationDeleteEvent(
+            id=snowflakes.Snowflake(payload["id"]),
             app=self._app,
             shard=shard,
             guild_id=snowflakes.Snowflake(payload["guild_id"]),
+            application_id=application_id,
+        )
+
+    def deserialize_integration_update_event(
+        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+    ) -> guild_events.IntegrationUpdateEvent:
+        return guild_events.IntegrationUpdateEvent(
+            app=self._app, shard=shard, integration=self._app.entity_factory.deserialize_integration(payload)
         )
 
     def deserialize_guild_member_add_event(
diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py
index 48695816..c92ad903 100644
--- a/hikari/impl/event_manager_base.py
+++ b/hikari/impl/event_manager_base.py
@@ -84,7 +84,7 @@ def consume_raw_event(
         try:
             callback = getattr(self, "on_" + event_name.lower())
         except AttributeError:
-            _LOGGER.debug("ignoring unknown event %s", event_name)
+            _LOGGER.debug("ignoring unknown event %s:\n    %r", event_name, payload)
         else:
             asyncio.create_task(self._handle_dispatch(callback, shard, payload), name=f"dispatch {event_name}")
 
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 8659e280..ce01ea57 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -2274,7 +2274,9 @@ async def fetch_integrations(
         route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild)
         raw_response = await self._request(route)
         response = typing.cast(data_binding.JSONArray, raw_response)
-        return data_binding.cast_json_array(response, self._entity_factory.deserialize_integration)
+        return data_binding.cast_json_array(
+            response, self._entity_factory.deserialize_integration, guild_id=snowflakes.Snowflake(guild)
+        )
 
     async def fetch_widget(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]) -> guilds.GuildWidget:
         route = routes.GET_GUILD_WIDGET.compile(guild=guild)
diff --git a/hikari/impl/stateful_event_manager.py b/hikari/impl/stateful_event_manager.py
index fffd100e..ec0c1bd0 100644
--- a/hikari/impl/stateful_event_manager.py
+++ b/hikari/impl/stateful_event_manager.py
@@ -208,11 +208,17 @@ async def on_guild_emojis_update(self, shard: gateway_shard.GatewayShard, payloa
 
         await self.dispatch(event)
 
-    async def on_guild_integrations_update(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> None:
-        """See https://discord.com/developers/docs/topics/gateway#guild-integrations-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_integrations_update_event(shard, payload))
+    async def on_integration_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        event = self._app.event_factory.deserialize_integration_create_event(shard, payload)
+        await self.dispatch(event)
+
+    async def on_integration_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        event = self._app.event_factory.deserialize_integration_delete_event(shard, payload)
+        await self.dispatch(event)
+
+    async def on_integration_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        event = self._app.event_factory.deserialize_integration_update_event(shard, payload)
+        await self.dispatch(event)
 
     async def on_guild_member_add(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-member-add for more info."""
diff --git a/hikari/impl/stateless_event_manager.py b/hikari/impl/stateless_event_manager.py
index ac24ad8f..884757b1 100644
--- a/hikari/impl/stateless_event_manager.py
+++ b/hikari/impl/stateless_event_manager.py
@@ -114,11 +114,14 @@ async def on_guild_emojis_update(self, shard: gateway_shard.GatewayShard, payloa
         """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info."""
         await self.dispatch(self._app.event_factory.deserialize_guild_emojis_update_event(shard, payload))
 
-    async def on_guild_integrations_update(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> None:
-        """See https://discord.com/developers/docs/topics/gateway#guild-integrations-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_integrations_update_event(shard, payload))
+    async def on_integration_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        await self.dispatch(self._app.event_factory.deserialize_integration_create_event(shard, payload))
+
+    async def on_integration_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        await self.dispatch(self._app.event_factory.deserialize_integration_delete_event(shard, payload))
+
+    async def on_integration_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
+        await self.dispatch(self._app.event_factory.deserialize_integration_update_event(shard, payload))
 
     async def on_guild_member_add(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-member-add for more info."""
diff --git a/hikari/intents.py b/hikari/intents.py
index 7150baa9..f14a52b3 100644
--- a/hikari/intents.py
+++ b/hikari/intents.py
@@ -136,7 +136,9 @@ class Intents(enums.Flag):
     - `GUILD_BAN_ADD`
     - `GUILD_BAN_REMOVE`
     - `GUILD_EMOJIS_UPDATE`
-    - `GUILD_INTEGRATIONS_UPDATE`
+    - `INTEGRATION_CREATE`
+    - `INTEGRATION_DELETE`
+    - `INTEGRATION_UPDATE`
     - `INVITE_CREATE`
     - `INVITE_DELETE`
     - `CHANNEL_CREATE`
@@ -211,7 +213,9 @@ class Intents(enums.Flag):
     GUILD_INTEGRATIONS = 1 << 4
     """Subscribes to the following events:
 
-    * `GUILD_INTEGRATIONS_UPDATE`
+    * `INTEGRATION_CREATE`
+    * `INTEGRATION_DELETE`
+    * `INTEGRATION_UPDATE`
     """
 
     GUILD_WEBHOOKS = 1 << 5
diff --git a/hikari/messages.py b/hikari/messages.py
index 3e1d292b..8b203898 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -40,20 +40,20 @@
 import attr
 
 from hikari import files
+from hikari import guilds
 from hikari import snowflakes
 from hikari import undefined
 from hikari import urls
 from hikari.internal import attr_extensions
 from hikari.internal import enums
+from hikari.internal import routes
 
 if typing.TYPE_CHECKING:
     import datetime
 
-    from hikari import applications
     from hikari import channels
     from hikari import embeds as embeds_
     from hikari import emojis as emojis_
-    from hikari import guilds
     from hikari import traits
     from hikari import users
 
@@ -244,6 +244,63 @@ class MessageCrosspost:
     """
 
 
+@attr_extensions.with_copy
+@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
+class MessageApplication(guilds.PartialApplication):
+    """The representation of an application used in messages."""
+
+    cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
+    """The CDN's hash of this application's cover image, used on the store."""
+
+    primary_sku_id: typing.Optional[snowflakes.Snowflake] = attr.ib(eq=False, hash=False, repr=False)
+    """The ID of the primary "Game SKU" of a game that's sold on Discord."""
+
+    @property
+    def cover_image_url(self) -> typing.Optional[files.URL]:
+        """Cover image used on the store.
+
+        Returns
+        -------
+        typing.Optional[hikari.files.URL]
+            The URL, or `builtins.None` if no cover image exists.
+        """
+        return self.format_cover_image()
+
+    def format_cover_image(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
+        """Generate the cover image used in the store, if set.
+
+        Parameters
+        ----------
+        ext : builtins.str
+            The extension to use for this URL, defaults to `png`.
+            Supports `png`, `jpeg`, `jpg` and `webp`.
+        size : builtins.int
+            The size to set for the URL, defaults to `4096`.
+            Can be any power of two between 16 and 4096.
+
+        Returns
+        -------
+        typing.Optional[hikari.files.URL]
+            The URL, or `builtins.None` if no cover image exists.
+
+        Raises
+        ------
+        builtins.ValueError
+            If the size is not an integer power of 2 between 16 and 4096
+            (inclusive).
+        """
+        if self.cover_image_hash is None:
+            return None
+
+        return routes.CDN_APPLICATION_COVER.compile_to_file(
+            urls.CDN_URL,
+            application_id=self.id,
+            hash=self.cover_image_hash,
+            size=size,
+            file_format=ext,
+        )
+
+
 @attr_extensions.with_copy
 @attr.s(slots=True, kw_only=True, init=True, repr=True, eq=False, weakref_slot=False)
 class PartialMessage(snowflakes.Unique):
@@ -338,7 +395,7 @@ class PartialMessage(snowflakes.Unique):
     activity: undefined.UndefinedNoneOr[MessageActivity] = attr.ib(repr=False)
     """The message's activity."""
 
-    application: undefined.UndefinedNoneOr[applications.Application] = attr.ib(repr=False)
+    application: undefined.UndefinedNoneOr[MessageApplication] = attr.ib(repr=False)
     """The message's application."""
 
     message_reference: undefined.UndefinedNoneOr[MessageCrosspost] = attr.ib(repr=False)
@@ -883,7 +940,7 @@ class Message(PartialMessage):
     activity: typing.Optional[MessageActivity]
     """The message activity."""
 
-    application: typing.Optional[applications.Application]
+    application: typing.Optional[MessageApplication]
     """The message application."""
 
     message_reference: typing.Optional[MessageCrosspost]


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 3b864b4f..00469de1 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -1552,6 +1552,7 @@ def integration_payload(self, user_payload):
             "name": "blaze it",
             "type": "youtube",
             "account": {"id": "6969", "name": "Blaze it"},
+            "guild_id": "9292929292",
             "enabled": True,
             "syncing": False,
             "revoked": True,
@@ -1581,6 +1582,7 @@ def integration_payload(self, user_payload):
     def test_deserialize_integration(self, entity_factory_impl, integration_payload, user_payload):
         integration = entity_factory_impl.deserialize_integration(integration_payload)
         assert integration.id == 420
+        assert integration.guild_id == 9292929292
         assert integration.name == "blaze it"
         assert integration.type == guild_models.IntegrationType.YOUTUBE
         # IntegrationAccount
@@ -1627,8 +1629,10 @@ def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_f
                 "enabled": True,
                 "role_id": None,
                 "synced_at": None,
-            }
+            },
+            guild_id=snowflakes.Snowflake(383838383883),
         )
+        assert integration.guild_id == 383838383883
         assert integration.is_emojis_enabled is None
         assert integration.role_id is None
         assert integration.last_synced_at is None
@@ -1644,6 +1648,7 @@ def test_deserialize_guild_integration_with_unset_bot(self, entity_factory_impl)
         integration = entity_factory_impl.deserialize_integration(
             {
                 "id": "420",
+                "guild_id": "929292929",
                 "name": "blaze it",
                 "type": "youtube",
                 "account": {"id": "6969", "name": "Blaze it"},
@@ -1656,6 +1661,8 @@ def test_deserialize_guild_integration_with_unset_bot(self, entity_factory_impl)
                     "icon": "123abc",
                     "summary": "same as desc",
                     "description": "same as desc2",
+                    "cover_image": "SMKLdiosa89123u",
+                    "primary_sku_id": "499494949494994",
                 },
             }
         )
@@ -2392,6 +2399,7 @@ def partial_application_payload(self):
             "icon": "2658b3029e775a931ffb49380073fa63",
             "cover_image": "58982a23790c4f22787b05d3be38a026",
             "summary": "asas",
+            "primary_sku_id": "499494949494994",
         }
 
     @pytest.fixture()
@@ -2505,7 +2513,15 @@ def test_deserialize_partial_message(
         assert partial_message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657"
         assert isinstance(partial_message.activity, message_models.MessageActivity)
 
-        assert partial_message.application == entity_factory_impl.deserialize_application(partial_application_payload)
+        # Message Activity
+        assert partial_message.application.id == 456
+        assert partial_message.application.name == "hikari"
+        assert partial_message.application.description == "The best application"
+        assert partial_message.application.icon_hash == "2658b3029e775a931ffb49380073fa63"
+        assert partial_message.application.summary == "asas"
+        assert partial_message.application.cover_image_hash == "58982a23790c4f22787b05d3be38a026"
+        assert partial_message.application.primary_sku_id == 499494949494994
+        assert isinstance(partial_message.application, message_models.MessageApplication)
         # MessageCrosspost
         assert partial_message.message_reference.app is mock_app
         assert partial_message.message_reference.id == 306588351130107906
@@ -2612,7 +2628,16 @@ def test_deserialize_full_message(
         assert message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657"
         assert isinstance(message.activity, message_models.MessageActivity)
 
-        assert message.application == entity_factory_impl.deserialize_application(partial_application_payload)
+        # MessageApplication
+        assert message.application.id == 456
+        assert message.application.name == "hikari"
+        assert message.application.description == "The best application"
+        assert message.application.icon_hash == "2658b3029e775a931ffb49380073fa63"
+        assert message.application.summary == "asas"
+        assert message.application.cover_image_hash == "58982a23790c4f22787b05d3be38a026"
+        assert message.application.primary_sku_id == 499494949494994
+        assert isinstance(message.application, message_models.MessageApplication)
+
         # MessageCrosspost
         assert message.message_reference.app is mock_app
         assert message.message_reference.id == 306588351130107906
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index fb9952cf..b70c2f1c 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -48,7 +48,7 @@ async def test_consume_raw_event_when_AttributeError(self, event_manager):
         with mock.patch.object(event_manager_base, "_LOGGER") as logger:
             event_manager.consume_raw_event(None, "UNEXISTING_EVENT", {})
 
-        logger.debug.assert_called_once_with("ignoring unknown event %s", "UNEXISTING_EVENT")
+        logger.debug.assert_called_once_with("ignoring unknown event %s:\n    %r", "UNEXISTING_EVENT", {})
 
     @pytest.mark.asyncio
     async def test_consume_raw_event_when_found(self, event_manager):
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 3f219f82..97a24bb8 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -2559,7 +2559,7 @@ async def test_fetch_integrations(self, rest_client):
         rest_client._request.assert_awaited_once_with(expected_route)
         assert rest_client._entity_factory.deserialize_integration.call_count == 2
         rest_client._entity_factory.deserialize_integration.assert_has_calls(
-            [mock.call({"id": "456"}), mock.call({"id": "789"})]
+            [mock.call({"id": "456"}, guild_id=123), mock.call({"id": "789"}, guild_id=123)]
         )
 
     async def test_fetch_widget(self, rest_client):
diff --git a/tests/hikari/impl/test_stateless_event_manager.py b/tests/hikari/impl/test_stateless_event_manager.py
index 9429bc93..e5d9e3b8 100644
--- a/tests/hikari/impl/test_stateless_event_manager.py
+++ b/tests/hikari/impl/test_stateless_event_manager.py
@@ -180,16 +180,32 @@ async def test_on_guild_emojis_update(self, event_manager, shard, payload):
         event_manager.dispatch.assert_awaited_once_with(event)
         event_manager._app.event_factory.deserialize_guild_emojis_update_event.assert_called_once_with(shard, payload)
 
-    async def test_on_guild_integrations_update(self, event_manager, shard, payload):
+    async def test_on_integration_create(self, event_manager, shard, payload):
         event = object()
-        event_manager._app.event_factory.deserialize_guild_integrations_update_event = mock.Mock(return_value=event)
+        event_manager._app.event_factory.deserialize_integration_create_event = mock.Mock(return_value=event)
 
-        await event_manager.on_guild_integrations_update(shard, payload)
+        await event_manager.on_integration_create(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_guild_integrations_update_event.assert_called_once_with(
-            shard, payload
-        )
+        event_manager._app.event_factory.deserialize_integration_create_event.assert_called_once_with(shard, payload)
+
+    async def test_on_integration_delete(self, event_manager, shard, payload):
+        event = object()
+        event_manager._app.event_factory.deserialize_integration_delete_event = mock.Mock(return_value=event)
+
+        await event_manager.on_integration_delete(shard, payload)
+
+        event_manager.dispatch.assert_awaited_once_with(event)
+        event_manager._app.event_factory.deserialize_integration_delete_event.assert_called_once_with(shard, payload)
+
+    async def test_on_integration_update(self, event_manager, shard, payload):
+        event = object()
+        event_manager._app.event_factory.deserialize_integration_update_event = mock.Mock(return_value=event)
+
+        await event_manager.on_integration_update(shard, payload)
+
+        event_manager.dispatch.assert_awaited_once_with(event)
+        event_manager._app.event_factory.deserialize_integration_update_event.assert_called_once_with(shard, payload)
 
     async def test_on_guild_member_add(self, event_manager, shard, payload):
         event = object()
diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py
index 0cec01cb..3f094674 100644
--- a/tests/hikari/test_applications.py
+++ b/tests/hikari/test_applications.py
@@ -55,12 +55,6 @@ def test_Team_str_operator():
     assert str(team) == "Team 696969"
 
 
-def test_Application_str_operator():
-    mock_application = mock.Mock(applications.Application)
-    mock_application.name = "beans"
-    assert applications.Application.__str__(mock_application) == "beans"
-
-
 class TestTeam:
     @pytest.fixture()
     def model(self):
@@ -108,33 +102,6 @@ def model(self):
             cover_image_hash="ahashcover",
         )()
 
-    def test_icon_url_property(self, model):
-        model.format_icon = mock.Mock(return_value="url")
-
-        assert model.icon_url == "url"
-
-        model.format_icon.assert_called_once_with()
-
-    def test_format_icon_when_hash_is_None(self, model):
-        model.icon_hash = None
-
-        with mock.patch.object(
-            routes, "CDN_APPLICATION_ICON", new=mock.Mock(compile_to_file=mock.Mock(return_value="file"))
-        ) as route:
-            assert model.format_icon(ext="jpeg", size=1) is None
-
-        route.compile_to_file.assert_not_called()
-
-    def test_format_icon_when_hash_is_not_None(self, model):
-        with mock.patch.object(
-            routes, "CDN_APPLICATION_ICON", new=mock.Mock(compile_to_file=mock.Mock(return_value="file"))
-        ) as route:
-            assert model.format_icon(ext="jpeg", size=1) == "file"
-
-        route.compile_to_file.assert_called_once_with(
-            urls.CDN_URL, application_id=123, hash="ahashicon", size=1, file_format="jpeg"
-        )
-
     def test_cover_image_url_property(self, model):
         model.format_cover_image = mock.Mock(return_value="url")
 
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 16c7df86..8b16118c 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -94,6 +94,51 @@ def test_str_operator(self, model):
         assert str(model) == "The Big Cool"
 
 
+def test_PartialApplication_str_operator():
+    mock_application = mock.Mock(guilds.PartialApplication)
+    mock_application.name = "beans"
+    assert guilds.PartialApplication.__str__(mock_application) == "beans"
+
+
+class TestPartialApplication:
+    @pytest.fixture()
+    def model(self):
+        return hikari_test_helpers.mock_class_namespace(
+            guilds.PartialApplication,
+            init_=False,
+            slots_=False,
+            id=123,
+            icon_hash="ahashicon",
+        )()
+
+    def test_icon_url_property(self, model):
+        model.format_icon = mock.Mock(return_value="url")
+
+        assert model.icon_url == "url"
+
+        model.format_icon.assert_called_once_with()
+
+    def test_format_icon_when_hash_is_None(self, model):
+        model.icon_hash = None
+
+        with mock.patch.object(
+            routes, "CDN_APPLICATION_ICON", new=mock.Mock(compile_to_file=mock.Mock(return_value="file"))
+        ) as route:
+            assert model.format_icon(ext="jpeg", size=1) is None
+
+        route.compile_to_file.assert_not_called()
+
+    def test_format_icon_when_hash_is_not_None(self, model):
+        with mock.patch.object(
+            routes, "CDN_APPLICATION_ICON", new=mock.Mock(compile_to_file=mock.Mock(return_value="file"))
+        ) as route:
+            assert model.format_icon(ext="jpeg", size=1) == "file"
+
+        route.compile_to_file.assert_called_once_with(
+            urls.CDN_URL, application_id=123, hash="ahashicon", size=1, file_format="jpeg"
+        )
+
+
 class TestIntegrationAccount:
     @pytest.fixture()
     def model(self, mock_app):