Repository: hikari-py/hikari
Pull request #1471 (2023-01-13 22:19:50)

Add support for audit log entry events
### Summary
<!-- Small summary of the merge request -->

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [ ] I have run `nox` and all the pipelines have passed.
- [ ] 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: Thanks for working on this @FasterSpeeding!

Patch:
diff --git a/changes/1471.deprecation.md b/changes/1471.deprecation.md
new file mode 100644
index 00000000..795fcffa
--- /dev/null
+++ b/changes/1471.deprecation.md
@@ -0,0 +1,1 @@
+Renamed `Intents.GUILD_BANS` to `Intents.GUILD_MODERATION`.
diff --git a/changes/1471.feature.md b/changes/1471.feature.md
new file mode 100644
index 00000000..1abda668
--- /dev/null
+++ b/changes/1471.feature.md
@@ -0,0 +1,1 @@
+Add support for guild audit log entry create events.
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/changes/1485.breaking.md b/changes/1485.breaking.md
new file mode 100644
index 00000000..db1d4823
--- /dev/null
+++ b/changes/1485.breaking.md
@@ -0,0 +1,1 @@
+Default logging to `sys.stdout` stream to bring more in-line with banner output.
diff --git a/changes/1485.bugfix.md b/changes/1485.bugfix.md
new file mode 100644
index 00000000..899ae373
--- /dev/null
+++ b/changes/1485.bugfix.md
@@ -0,0 +1,1 @@
+Fix colour logging not occurring on specific terminals (ie, Pycharm).
diff --git a/changes/1485.feature.md b/changes/1485.feature.md
new file mode 100644
index 00000000..3d47417e
--- /dev/null
+++ b/changes/1485.feature.md
@@ -0,0 +1,1 @@
+Support loading files through `logging.config.fileConfig` in `init_logging`.
diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py
index 44972157..b0b747df 100644
--- a/hikari/api/entity_factory.py
+++ b/hikari/api/entity_factory.py
@@ -240,13 +240,17 @@ def deserialize_implicit_token(self, query: data_binding.Query) -> application_m
     #####################
 
     @abc.abstractmethod
-    def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_models.AuditLog:
+    def deserialize_audit_log(
+        self, payload: data_binding.JSONObject, *, guild_id: snowflakes.Snowflake
+    ) -> audit_log_models.AuditLog:
         """Parse a raw payload from Discord into an audit log object.
 
         Parameters
         ----------
         payload : hikari.internal.data_binding.JSONObject
             The JSON payload to deserialize.
+        guild_id : hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake]
+            The ID of the guild this audit log belongs to.
 
         Returns
         -------
@@ -254,6 +258,38 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_m
             The deserialized audit log object.
         """
 
+    @abc.abstractmethod
+    def deserialize_audit_log_entry(
+        self,
+        payload: data_binding.JSONObject,
+        *,
+        guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
+    ) -> audit_log_models.AuditLogEntry:
+        """Parse a raw payload from Discord into an audit log entry object.
+
+        Parameters
+        ----------
+        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 entry belongs to. If passed then this
+            will be prioritised over `"guild_id"` in the payload.
+
+        Returns
+        -------
+        hikari.audit_logs.AuditLogEntry
+            The deserialized audit log entry object.
+
+        Raises
+        ------
+        KeyError
+            If `guild_id` is left as `hikari.undefined.UNDEFINED` when
+            `"guild_id"` is not present in the passed payload.
+        """
+
     ##################
     # CHANNEL MODELS #
     ##################
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index 955fe9f3..438f96e6 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -674,6 +674,27 @@ def deserialize_presence_update_event(
             The parsed presence update event object.
         """
 
+    @abc.abstractmethod
+    def deserialize_audit_log_entry_create_event(
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+    ) -> guild_events.AuditLogEntryCreateEvent:
+        """Parse a raw payload from Discord into a audit log entry create 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.AuditLogEntryCreateEvent
+            The parsed audit log entry create object.
+        """
+
     ######################
     # INTERACTION EVENTS #
     ######################
diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py
index d7a3f836..b3fe8382 100644
--- a/hikari/audit_logs.py
+++ b/hikari/audit_logs.py
@@ -573,6 +573,9 @@ class AuditLogEntry(snowflakes.Unique):
     id: snowflakes.Snowflake = attr.field(hash=True, repr=True)
     """The ID of this entity."""
 
+    guild_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True)
+    """ID of the guild this audit log entry is for."""
+
     target_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=True)
     """The ID of the entity affected by this change, if applicable."""
 
diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py
index 1e044844..e73458e4 100644
--- a/hikari/events/guild_events.py
+++ b/hikari/events/guild_events.py
@@ -25,6 +25,7 @@
 from __future__ import annotations
 
 __all__: typing.Sequence[str] = (
+    "AuditLogEntryCreateEvent",
     "GuildEvent",
     "GuildVisibilityEvent",
     "GuildAvailableEvent",
@@ -56,6 +57,7 @@
 from hikari.internal import attr_extensions
 
 if typing.TYPE_CHECKING:
+    from hikari import audit_logs
     from hikari import channels as channels_
     from hikari import emojis as emojis_
     from hikari import guilds
@@ -68,7 +70,10 @@
 
 
 @base_events.requires_intents(
-    intents.Intents.GUILDS, intents.Intents.GUILD_BANS, intents.Intents.GUILD_EMOJIS, intents.Intents.GUILD_PRESENCES
+    intents.Intents.GUILDS,
+    intents.Intents.GUILD_MODERATION,
+    intents.Intents.GUILD_EMOJIS,
+    intents.Intents.GUILD_PRESENCES,
 )
 class GuildEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any guild-bound event."""
@@ -339,7 +344,7 @@ def guild_id(self) -> snowflakes.Snowflake:
         return self.guild.id
 
 
-@base_events.requires_intents(intents.Intents.GUILD_BANS)
+@base_events.requires_intents(intents.Intents.GUILD_MODERATION)
 class BanEvent(GuildEvent, abc.ABC):
     """Event base for any guild ban or unban."""
 
@@ -373,7 +378,7 @@ async def fetch_user(self) -> users.User:
 
 @attr_extensions.with_copy
 @attr.define(kw_only=True, weakref_slot=False)
-@base_events.requires_intents(intents.Intents.GUILD_BANS)
+@base_events.requires_intents(intents.Intents.GUILD_MODERATION)
 class BanCreateEvent(BanEvent):
     """Event that is fired when a user is banned from a guild."""
 
@@ -402,7 +407,7 @@ async def fetch_ban(self) -> guilds.GuildBan:
 
 @attr_extensions.with_copy
 @attr.define(kw_only=True, weakref_slot=False)
-@base_events.requires_intents(intents.Intents.GUILD_BANS)
+@base_events.requires_intents(intents.Intents.GUILD_MODERATION)
 class BanDeleteEvent(BanEvent):
     """Event that is fired when a user is unbanned from a guild."""
 
@@ -682,3 +687,26 @@ async def fetch_user(self) -> users.User:
             The user affected by this event.
         """
         return await self.app.rest.fetch_user(self.user_id)
+
+
+@attr_extensions.with_copy
+@attr.define(kw_only=True, weakref_slot=False)
+@base_events.requires_intents(intents.Intents.GUILD_MODERATION)
+class AuditLogEntryCreateEvent(GuildEvent):
+    """Event sent when a guild audit log entry was created."""
+
+    shard: gateway_shard.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from ShardEvent>>.
+
+    entry: audit_logs.AuditLogEntry = attr.field()
+    """The created entry."""
+
+    @property
+    def app(self) -> traits.RESTAware:
+        # <<inherited docstring from Event>>.
+        return self.entry.app
+
+    @property
+    def guild_id(self) -> snowflakes.Snowflake:
+        # <<inherited docstring from GuildEvent>>.
+        return self.entry.guild_id
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index e597d4d3..220008d1 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -57,6 +57,7 @@
 
 if typing.TYPE_CHECKING:
     import concurrent.futures
+    import os
 
     from hikari import channels
     from hikari import guilds
@@ -194,30 +195,30 @@ class GatewayBot(traits.GatewayBotAware):
            we should also chunk small guilds if the presences are not declared).
         4. The members cache is enabled or there are listeners for the
            `MemberChunkEvent`.
-    logs : typing.Union[None, LoggerLevel, typing.Dict[str, typing.Any]]
-        Defaults to `"INFO"`.
+    logs : typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike]
+        The flavour to set the logging to.
 
-        If `None`, then the Python logging system is left uninitialized
-        on startup, and you will need to configure it manually to view most
-        logs that are output by components of this library.
+        This can be `None` to not enable logging automatically.
 
-        If one of the valid values in a `LoggerLevel`, then this will match a
-        call to `colorlog.basicConfig` (a facade for `logging.basicConfig` with
-        additional conduit for enabling coloured logging levels) with the
-        `level` kwarg matching this value.
+        If you pass a `str` or a `int`, it is interpreted as
+        the global logging level to use, and should match one of `"DEBUG"`,
+        `"INFO"`, `"WARNING"`, `"ERROR"` or `"CRITICAL"`.
+        The configuration will be set up to use a `colorlog` coloured logger,
+        and to use a sane logging format strategy. The output will be written
+        to `sys.stdout` using this configuration.
 
-        If a `typing.Dict[str, typing.Any]` equivalent, then this value is
-        passed to `logging.config.dictConfig` to allow the user to provide a
-        specialized logging configuration of their choice. If any handlers are
-        defined in the dict, default handlers will not be setup.
+        If you pass a `dict`, it is treated as the mapping to pass to
+        `logging.config.dictConfig`. If the dict defines any handlers, default
+        handlers will not be setup if `incremental` is not specified.
 
-        As a side note, you can always opt to leave this on the default value
-        and then use an incremental `logging.config.dictConfig` that applies
-        any additional changes on top of the base configuration, if you prefer.
-        An example of can be found in the `Example` section.
+        If you pass a `str` to an existing file or a `os.PathLike`, it is
+        interpreted as the file to load config from using `logging.config.fileConfig`.
 
         Note that `"TRACE_HIKARI"` is a library-specific logging level
         which is expected to be more verbose than `"DEBUG"`.
+
+        Defaults to `"INFO"`.
+
     max_rate_limit : float
         The max number of seconds to backoff for when rate limited. Anything
         greater than this will instead raise an error.
@@ -245,25 +246,36 @@ class GatewayBot(traits.GatewayBotAware):
 
     Examples
     --------
-    Setting up logging using a dictionary configuration:
+    Simple logging setup:
 
     .. code-block:: python
 
-        import os
+        hikari.GatewayBot("TOKEN", logs="INFO")  # Registered logging level
+        # or
+        hikari.GatewayBot("TOKEN", logs=20)  # Logging level as an int
+
+    File config:
 
-        import hikari
+    .. code-block:: python
 
-        # We want to make gateway logs output as DEBUG, and TRACE for all ratelimit content.
-        bot = hikari.GatewayBot(
-            token=os.environ["BOT_TOKEN"],
+        # See https://docs.python.org/3/library/logging.config.html#configuration-file-format for more info
+        hikari.GatewayBot("TOKEN", logs="path/to/file.ini")
+
+    Setting up logging through a dict config:
+
+    .. code-block:: python
+
+        # See https://docs.python.org/3/library/logging.config.html#dictionary-schema-details for more info
+        hikari.GatewayBot(
+            "TOKEN",
             logs={
                 "version": 1,
-                "incremental": True,
+                "incremental": True,  # In incremental setups, the default stream handler will be setup
                 "loggers": {
                     "hikari.gateway": {"level": "DEBUG"},
                     "hikari.ratelimits": {"level": "TRACE_HIKARI"},
                 },
-            },
+            }
         )
     """
 
@@ -307,7 +319,7 @@ def __init__(
         http_settings: typing.Optional[config_impl.HTTPSettings] = None,
         intents: intents_.Intents = intents_.Intents.ALL_UNPRIVILEGED,
         auto_chunk_members: bool = True,
-        logs: typing.Union[None, int, str, typing.Dict[str, typing.Any]] = "INFO",
+        logs: typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]] = "INFO",
         max_rate_limit: float = 300.0,
         max_retries: int = 3,
         proxy_settings: typing.Optional[config_impl.ProxySettings] = None,
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 497b927f..c60c933a 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -767,61 +767,81 @@ def _deserialize_member_move_entry_info(
             app=self._app, channel_id=snowflakes.Snowflake(payload["channel_id"]), count=int(payload["count"])
         )
 
-    def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_models.AuditLog:
-        entries = {}
-        for entry_payload in payload["audit_log_entries"]:
-            entry_id = snowflakes.Snowflake(entry_payload["id"])
+    def deserialize_audit_log_entry(
+        self,
+        payload: data_binding.JSONObject,
+        *,
+        guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
+    ) -> audit_log_models.AuditLogEntry:
+        if guild_id is undefined.UNDEFINED:
+            guild_id = snowflakes.Snowflake(payload["guild_id"])
 
-            changes: typing.List[audit_log_models.AuditLogChange] = []
-            if (change_payloads := entry_payload.get("changes")) is not None:
-                for change_payload in change_payloads:
-                    key: typing.Union[audit_log_models.AuditLogChangeKey, str] = audit_log_models.AuditLogChangeKey(
-                        change_payload["key"]
-                    )
+        entry_id = snowflakes.Snowflake(payload["id"])
 
-                    new_value = change_payload.get("new_value")
-                    old_value = change_payload.get("old_value")
-                    if value_converter := self._audit_log_entry_converters.get(key):
-                        new_value = value_converter(new_value) if new_value is not None else None
-                        old_value = value_converter(old_value) if old_value is not None else None
+        changes: typing.List[audit_log_models.AuditLogChange] = []
+        if (change_payloads := payload.get("changes")) is not None:
+            for change_payload in change_payloads:
+                key: typing.Union[audit_log_models.AuditLogChangeKey, str] = audit_log_models.AuditLogChangeKey(
+                    change_payload["key"]
+                )
 
-                    elif not isinstance(
-                        key, audit_log_models.AuditLogChangeKey
-                    ):  # pyright: ignore [reportUnnecessaryIsInstance]
-                        _LOGGER.debug("Unknown audit log change key found %r", key)
+                new_value = change_payload.get("new_value")
+                old_value = change_payload.get("old_value")
+                if value_converter := self._audit_log_entry_converters.get(key):
+                    new_value = value_converter(new_value) if new_value is not None else None
+                    old_value = value_converter(old_value) if old_value is not None else None
 
-                    changes.append(audit_log_models.AuditLogChange(key=key, new_value=new_value, old_value=old_value))
+                elif not isinstance(
+                    key, audit_log_models.AuditLogChangeKey
+                ):  # pyright: ignore [reportUnnecessaryIsInstance]
+                    _LOGGER.debug("Unknown audit log change key found %r", key)
 
-            target_id: typing.Optional[snowflakes.Snowflake] = None
-            if (raw_target_id := entry_payload["target_id"]) is not None:
-                target_id = snowflakes.Snowflake(raw_target_id)
+                changes.append(audit_log_models.AuditLogChange(key=key, new_value=new_value, old_value=old_value))
 
-            user_id: typing.Optional[snowflakes.Snowflake] = None
-            if (raw_user_id := entry_payload["user_id"]) is not None:
-                user_id = snowflakes.Snowflake(raw_user_id)
+        target_id: typing.Optional[snowflakes.Snowflake] = None
+        if (raw_target_id := payload["target_id"]) is not None:
+            target_id = snowflakes.Snowflake(raw_target_id)
 
-            action_type: typing.Union[audit_log_models.AuditLogEventType, int]
-            action_type = audit_log_models.AuditLogEventType(entry_payload["action_type"])
+        user_id: typing.Optional[snowflakes.Snowflake] = None
+        if (raw_user_id := payload["user_id"]) is not None:
+            user_id = snowflakes.Snowflake(raw_user_id)
 
-            options: typing.Optional[audit_log_models.BaseAuditLogEntryInfo] = None
-            if (raw_option := entry_payload.get("options")) is not None:
-                if option_converter := self._audit_log_event_mapping.get(action_type):
-                    options = option_converter(raw_option)
+        action_type: typing.Union[audit_log_models.AuditLogEventType, int]
+        action_type = audit_log_models.AuditLogEventType(payload["action_type"])
 
-                else:
-                    _LOGGER.debug("Unknown audit log action type found %r", action_type)
-                    continue
+        options: typing.Optional[audit_log_models.BaseAuditLogEntryInfo] = None
+        if (raw_option := payload.get("options")) is not None:
+            if option_converter := self._audit_log_event_mapping.get(action_type):
+                options = option_converter(raw_option)
 
-            entries[entry_id] = audit_log_models.AuditLogEntry(
-                app=self._app,
-                id=entry_id,
-                target_id=target_id,
-                changes=changes,
-                user_id=user_id,
-                action_type=action_type,
-                options=options,
-                reason=entry_payload.get("reason"),
-            )
+            else:
+                raise errors.UnrecognisedEntityError(f"Unknown audit log action type found {action_type!r}")
+
+        return audit_log_models.AuditLogEntry(
+            app=self._app,
+            id=entry_id,
+            target_id=target_id,
+            changes=changes,
+            user_id=user_id,
+            action_type=action_type,
+            options=options,
+            reason=payload.get("reason"),
+            guild_id=guild_id,
+        )
+
+    def deserialize_audit_log(
+        self, payload: data_binding.JSONObject, *, guild_id: snowflakes.Snowflake
+    ) -> audit_log_models.AuditLog:
+        entries = {}
+        for entry_payload in payload["audit_log_entries"]:
+            try:
+                entry = self.deserialize_audit_log_entry(entry_payload, guild_id=guild_id)
+
+            except errors.UnrecognisedEntityError as exc:
+                _LOGGER.debug(exc.reason)
+
+            else:
+                entries[entry.id] = entry
 
         integrations = {
             snowflakes.Snowflake(integration["id"]): self.deserialize_partial_integration(integration)
@@ -3524,21 +3544,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/impl/event_factory.py b/hikari/impl/event_factory.py
index 5ea510d3..0eb69e51 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -486,6 +486,15 @@ def deserialize_presence_update_event(
             )
         return guild_events.PresenceUpdateEvent(shard=shard, presence=presence, user=user, old_presence=old_presence)
 
+    def deserialize_audit_log_entry_create_event(
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+    ) -> guild_events.AuditLogEntryCreateEvent:
+        return guild_events.AuditLogEntryCreateEvent(
+            shard=shard, entry=self._app.entity_factory.deserialize_audit_log_entry(payload)
+        )
+
     ######################
     # INTERACTION EVENTS #
     ######################
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index e683e742..ef1c6590 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -844,3 +844,10 @@ async def on_guild_scheduled_event_user_remove(
     ) -> None:
         """See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-remove for more info."""
         await self.dispatch(self._event_factory.deserialize_scheduled_event_user_remove_event(shard, payload))
+
+    @event_manager_base.filtered(guild_events.AuditLogEntryCreateEvent)
+    async def on_guild_audit_log_entry_create(
+        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+    ) -> None:
+        """See https://discord.com/developers/docs/topics/gateway-events#guild-audit-log-entry-create for more info."""
+        await self.dispatch(self._event_factory.deserialize_audit_log_entry_create_event(shard, payload))
diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py
index 8ee35fbc..8a56ed90 100644
--- a/hikari/impl/rest_bot.py
+++ b/hikari/impl/rest_bot.py
@@ -43,6 +43,7 @@
 
 if typing.TYPE_CHECKING:
     import concurrent.futures
+    import os
     import socket as socket_
     import ssl
 
@@ -127,30 +128,29 @@ class RESTBot(traits.RESTBotAware, interaction_server_.InteractionServer):
         customise functionality such as whether SSL-verification is enabled,
         what timeouts `aiohttp` should expect to use for requests, and behavior
         regarding HTTP-redirects.
-    logs : typing.Union[None, LoggerLevel, typing.Dict[str, typing.Any]]
-        Defaults to `"INFO"`.
+    logs : typing.Union[None, int, str, typing.Dict[str, typing.Any], os.PathLike[str]]
+        The flavour to set the logging to.
 
-        If `None`, then the Python logging system is left uninitialized
-        on startup, and you will need to configure it manually to view most
-        logs that are output by components of this library.
+        This can be `None` to not enable logging automatically.
 
-        If one of the valid values in a `LoggerLevel`, then this will match a
-        call to `colorlog.basicConfig` (a facade for `logging.basicConfig` with
-        additional conduit for enabling coloured logging levels) with the
-        `level` kwarg matching this value.
+        If you pass a `str` or a `int`, it is interpreted as
+        the global logging level to use, and should match one of `"DEBUG"`,
+        `"INFO"`, `"WARNING"`, `"ERROR"` or `"CRITICAL"`.
+        The configuration will be set up to use a `colorlog` coloured logger,
+        and to use a sane logging format strategy. The output will be written
+        to `sys.stdout` using this configuration.
 
-        If a `typing.Dict[str, typing.Any]` equivalent, then this value is
-        passed to `logging.config.dictConfig` to allow the user to provide a
-        specialized logging configuration of their choice. If any handlers are
-        defined in the dict, default handlers will not be setup.
+        If you pass a `dict`, it is treated as the mapping to pass to
+        `logging.config.dictConfig`. If the dict defines any handlers, default
+        handlers will not be setup if `incremental` is not specified.
 
-        As a side note, you can always opt to leave this on the default value
-        and then use an incremental `logging.config.dictConfig` that applies
-        any additional changes on top of the base configuration, if you prefer.
-        An example of can be found in the `Example` section.
+        If you pass a `str` to an existing file or a `os.PathLike`, it is
+        interpreted as the file to load config from using `logging.config.fileConfig`.
 
         Note that `"TRACE_HIKARI"` is a library-specific logging level
         which is expected to be more verbose than `"DEBUG"`.
+
+        Defaults to `"INFO"`.
     max_rate_limit : float
         The max number of seconds to backoff for when rate limited. Anything
         greater than this will instead raise an error.
@@ -186,6 +186,40 @@ class RESTBot(traits.RESTBotAware, interaction_server_.InteractionServer):
     ValueError
         * If `token_type` is provided when a token strategy is passed for `token`.
         * if `token_type` is left as `None` when a string is passed for `token`.
+
+    Examples
+    --------
+    Simple logging setup:
+
+    .. code-block:: python
+
+        hikari.RESTBot("TOKEN", logs="INFO")  # Registered logging level
+        # or
+        hikari.RESTBot("TOKEN", logs=20)  # Logging level as an int
+
+    File config:
+
+    .. code-block:: python
+
+        # See https://docs.python.org/3/library/logging.config.html#configuration-file-format for more info
+        hikari.RESTBot("TOKEN", logs="path/to/file.ini")
+
+    Setting up logging through a dict config:
+
+    .. code-block:: python
+
+        # See https://docs.python.org/3/library/logging.config.html#dictionary-schema-details for more info
+        hikari.RESTBot(
+            "TOKEN",
+            logs={
+                "version": 1,
+                "incremental": True,  # In incremental setups, the default stream handler will be setup
+                "loggers": {
+                    "hikari.gateway": {"level": "DEBUG"},
+                    "hikari.ratelimits": {"level": "TRACE_HIKARI"},
+                },
+            }
+        )
     """
 
     __slots__: typing.Sequence[str] = (
@@ -213,7 +247,7 @@ def __init__(
         executor: typing.Optional[concurrent.futures.Executor] = None,
         force_color: bool = False,
         http_settings: typing.Optional[config_impl.HTTPSettings] = None,
-        logs: typing.Union[None, int, str, typing.Dict[str, typing.Any]] = "INFO",
+        logs: typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]] = "INFO",
         max_rate_limit: float = 300.0,
         max_retries: int = 3,
         proxy_settings: typing.Optional[config_impl.ProxySettings] = None,
@@ -234,7 +268,7 @@ def __init__(
         executor: typing.Optional[concurrent.futures.Executor] = None,
         force_color: bool = False,
         http_settings: typing.Optional[config_impl.HTTPSettings] = None,
-        logs: typing.Union[None, int, str, typing.Dict[str, typing.Any]] = "INFO",
+        logs: typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]] = "INFO",
         max_rate_limit: float = 300.0,
         max_retries: int = 3,
         proxy_settings: typing.Optional[config_impl.ProxySettings] = None,
@@ -254,7 +288,7 @@ def __init__(
         executor: typing.Optional[concurrent.futures.Executor] = None,
         force_color: bool = False,
         http_settings: typing.Optional[config_impl.HTTPSettings] = None,
-        logs: typing.Union[None, int, str, typing.Dict[str, typing.Any]] = "INFO",
+        logs: typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]] = "INFO",
         max_rate_limit: float = 300.0,
         max_retries: int = 3,
         proxy_settings: typing.Optional[config_impl.ProxySettings] = None,
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index a2023c90..3d9bb275 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -780,6 +780,7 @@ class AuditLogIterator(iterators.LazyIterator["audit_logs.AuditLog"]):
     __slots__: typing.Sequence[str] = (
         "_entity_factory",
         "_action_type",
+        "_guild_id",
         "_request_call",
         "_route",
         "_first_id",
@@ -798,6 +799,7 @@ def __init__(
         self._action_type = action_type
         self._entity_factory = entity_factory
         self._first_id = before
+        self._guild_id = snowflakes.Snowflake(guild)
         self._request_call = request_call
         self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild)
         self._user = user
@@ -816,7 +818,7 @@ async def __anext__(self) -> audit_logs.AuditLog:
         if not audit_log_entries:
             raise StopAsyncIteration
 
-        log = self._entity_factory.deserialize_audit_log(response)
+        log = self._entity_factory.deserialize_audit_log(response, guild_id=self._guild_id)
         # Since deserialize_audit_log may skip entries it doesn't recognise,
         # first_id has to be calculated based on the raw payload as log.entries
         # may be missing entries.
diff --git a/hikari/intents.py b/hikari/intents.py
index aaad9d71..0e015255 100644
--- a/hikari/intents.py
+++ b/hikari/intents.py
@@ -79,10 +79,10 @@ class Intents(enums.Flag):
         # so to keep your code readable.
         my_intents = (
             Intents.GUILDS             |
-            Intents.GUILD_BANS         |
             Intents.GUILD_EMOJIS       |
             Intents.GUILD_INTEGRATIONS |
             Intents.GUILD_MESSAGES     |
+            Intents.GUILD_MODERATION   |
             Intents.PRIVATE_MESSAGES
         )
 
@@ -219,13 +219,17 @@ class Intents(enums.Flag):
         This intent is privileged, and requires enabling/whitelisting to use.
     """
 
-    GUILD_BANS = 1 << 2
+    GUILD_MODERATION = 1 << 2
     """Subscribes to the events listed below.
 
+    * `GUILD_AUDIT_LOG_ENTRY_CREATE`
     * `GUILD_BAN_ADD`
     * `GUILD_BAN_REMOVE`
     """
 
+    GUILD_BANS = enums.deprecated(GUILD_MODERATION, removal_version="2.0.0.dev118")
+    """Deprecated alias of `GUILD_BANS`."""
+
     GUILD_EMOJIS = 1 << 3
     """Subscribes to the events listed below.
 
@@ -341,7 +345,6 @@ class Intents(enums.Flag):
 
     ALL_GUILDS_UNPRIVILEGED = (
         GUILDS
-        | GUILD_BANS
         | GUILD_EMOJIS
         | GUILD_INTEGRATIONS
         | GUILD_WEBHOOKS
@@ -350,6 +353,7 @@ class Intents(enums.Flag):
         | GUILD_MESSAGES
         | GUILD_MESSAGE_REACTIONS
         | GUILD_MESSAGE_TYPING
+        | GUILD_MODERATION
         | GUILD_SCHEDULED_EVENTS
     )
     """All unprivileged guild-related intents."""
diff --git a/hikari/internal/enums.py b/hikari/internal/enums.py
index caa96e63..fc1def29 100644
--- a/hikari/internal/enums.py
+++ b/hikari/internal/enums.py
@@ -402,7 +402,10 @@ def __call__(cls, value: int = 0) -> typing.Any:
                 return pseudomember
 
     def __getitem__(cls, name: str) -> typing.Any:
-        return cls._name_to_member_map_[name]
+        if member := getattr(cls, name, None):
+            return member
+
+        raise KeyError(name)
 
     def __iter__(cls) -> typing.Iterator[typing.Any]:
         yield from cls._name_to_member_map_.values()
@@ -466,21 +469,24 @@ def __new__(
         cls = super().__new__(mcs, name, (int, *bases), new_namespace)
 
         for name, value in namespace.names_to_values.items():
-            # Patching the member init call is around 100ns faster per call than
-            # using the default type.__call__ which would make us do the lookup
-            # in cls.__new__. Reason for this is that python will also always
-            # invoke cls.__init__ if we do this, so we end up with two function
-            # calls.
-            member = cls.__new__(cls, value)
-            member._name_ = name
-            member._value_ = value
+            member = new_namespace.get(name)
+            if not isinstance(member, _DeprecatedAlias):
+                # Patching the member init call is around 100ns faster per call than
+                # using the default type.__call__ which would make us do the lookup
+                # in cls.__new__. Reason for this is that python will also always
+                # invoke cls.__init__ if we do this, so we end up with two function
+                # calls.
+                member = cls.__new__(cls, value)
+                member._name_ = name
+                member._value_ = value
+                setattr(cls, name, member)
+
+                if not (value & value - 1):
+                    powers_of_2_map[value] = member
+
             name_to_member[name] = member
-            value_to_member[value] = member
+            value_to_member.setdefault(value, member)
             member_names.append(name)
-            setattr(cls, name, member)
-
-            if not (value & value - 1):
-                powers_of_2_map[value] = member
 
         all_bits = functools.reduce(operator.or_, value_to_member.keys())
         all_bits_member = cls.__new__(cls, all_bits)
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index b7e0c459..a1a78127 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -36,6 +36,7 @@
 import logging
 import logging.config
 import os
+import pathlib
 import platform
 import re
 import string
@@ -67,33 +68,44 @@
 
 
 def init_logging(
-    flavor: typing.Union[None, str, int, typing.Dict[str, typing.Any]],
+    flavor: typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]],
     allow_color: bool,
     force_color: bool,
 ) -> None:
-    """Attempt to initialize logging for the user.
+    """Initialize logging for the user.
 
-    If any handlers already exist, this is ignored entirely. This ensures the
-    user can use any existing logging configuration without us interfering.
-    You can manually disable this by passing `None` as the `flavor` parameter.
+    .. note::
+        If any handlers already exist, some opinionated defaults will be configured
+        (mostly do to with logging efficiency and warning logging), but existing
+        handlers will not be overwritten. You can disable this by passing
+        `None` as the `flavor` parameter.
+
+    .. warning::
+        This function is blocking!
 
     Parameters
     ----------
-    flavor : typing.Optional[None, str, typing.Dict[str, typing.Any]]
+    flavor : typing.Optional[None, str, int, typing.Dict[str, typing.Any], os.PathLike[str]]
         The hint for configuring logging.
 
         This can be `None` to not enable logging automatically.
 
         If you pass a `str` or a `int`, it is interpreted as
         the global logging level to use, and should match one of `"DEBUG"`,
-        `"INFO"`, `"WARNING"`, `"ERROR"` or `"CRITICAL"`, if `str`.
+        `"INFO"`, `"WARNING"`, `"ERROR"` or `"CRITICAL"`.
         The configuration will be set up to use a `colorlog` coloured logger,
         and to use a sane logging format strategy. The output will be written
-        to `sys.stderr` using this configuration.
+        to `sys.stdout` using this configuration.
 
         If you pass a `dict`, it is treated as the mapping to pass to
         `logging.config.dictConfig`. If the dict defines any handlers, default
-        handlers will not be setup.
+        handlers will not be setup if `incremental` is not specified.
+
+        If you pass a `str` to an existing file or a `os.PathLike`, it is
+        interpreted as the file to load config from using `logging.config.fileConfig`.
+
+        Note that `"TRACE_HIKARI"` is a library-specific logging level
+        which is expected to be more verbose than `"DEBUG"`.
     allow_color : bool
         If `False`, no colour is allowed. If `True`, the
         output device must be supported for this to return `True`.
@@ -101,6 +113,39 @@ def init_logging(
         If `True`, return `True` always, otherwise only
         return `True` if the device supports colour output and the
         `allow_color` flag is not `False`.
+
+    Examples
+    --------
+    Simple logging setup:
+
+    .. code-block:: python
+
+        init_logging("INFO")  # Registered logging level
+        # or
+        init_logging(20)  # Logging level as an int
+
+    File config:
+
+    .. code-block:: python
+
+        # See https://docs.python.org/3/library/logging.config.html#configuration-file-format for more info
+        init_logging("path/to/file.ini")
+
+    Setting up logging through a dict config:
+
+    .. code-block:: python
+
+        # See https://docs.python.org/3/library/logging.config.html#dictionary-schema-details for more info
+        init_logging(
+            {
+                "version": 1,
+                "incremental": True,  # In incremental setups, the default stream handler will be setup
+                "loggers": {
+                    "hikari.gateway": {"level": "DEBUG"},
+                    "hikari.ratelimits": {"level": "TRACE_HIKARI"},
+                },
+            }
+        )
     """
     # One observation that has been repeatedly made from seeing beginners writing
     # bots in Python is that most people seem to have no idea what logging is or
@@ -113,41 +158,79 @@ def init_logging(
     # As part of Hikari's set of opinionated defaults, we turn logging on with
     # a desirable format that is coloured in an effort to draw the user's attention
     # to it, rather than encouraging them to ignore it.
+    if flavor is None:
+        return
 
-    if len(logging.root.handlers) != 0 or flavor is None:
-        # Skip, the user is using something else to configure their logging.
+    # Apparently this makes logging even more efficient!
+    logging.logThreads = False
+    logging.logProcesses = False
+    logging.logMultiprocessing = False
+
+    # DeprecationWarning is disabled by default, but it's useful to have enabled
+    warnings.simplefilter("always", DeprecationWarning)
+    logging.captureWarnings(True)
+
+    if len(logging.root.handlers) != 0:
+        # Something else is already setup, so don't overwrite it
+        return
+
+    if isinstance(flavor, str):
+        # Syntactic sugar to allow paths as strings
+        path = pathlib.Path(flavor)
+        if path.expanduser().exists():
+            flavor = path
+
+    # Config through file
+    if isinstance(flavor, os.PathLike):
+        try:
+            logging.config.fileConfig(flavor)
+        except Exception as ex:
+            raise RuntimeError("A problem occurred while trying to setup logging through file configuration") from ex
         return
 
+    # Config through dict
     if isinstance(flavor, dict):
-        logging.config.dictConfig(flavor)
+        try:
+            logging.config.dictConfig(flavor)
+        except Exception as ex:
+            raise RuntimeError("A problem occurred while trying to setup logging through dict configuration") from ex
 
-        if flavor.get("handlers"):
-            # Handlers are defined => don't configure the default ones
+        if not flavor.get("incremental"):
+            # Non-incremental setup, return
             return
 
         flavor = None
 
-    # Apparently this makes logging even more efficient!
-    logging.logThreads = False
-    logging.logProcesses = False
-
-    if supports_color(allow_color, force_color):
-        colorlog.basicConfig(
-            level=flavor,
-            format="%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: "
-            "%(thin)s%(message)s%(reset)s",
-            stream=sys.stderr,
-        )
-    else:
-        logging.basicConfig(
-            level=flavor,
-            format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s",
-            stream=sys.stderr,
-        )
+    # Default config (stream)
+    try:
+        if supports_color(allow_color, force_color):
+            logging.basicConfig(level=flavor, stream=sys.stdout)
+            handler = logging.root.handlers[0]
+            handler.setFormatter(
+                colorlog.formatter.ColoredFormatter(
+                    fmt=(
+                        "%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s "  # Logging level
+                        "%(asctime)23.23s "  # Date and time
+                        "%(bold)s%(name)s: "  # Logger name
+                        "%(thin)s%(message)s%(reset)s"  # Message
+                    ),
+                    force_color=True,
+                )
+            )
+        else:
+            logging.basicConfig(
+                level=flavor,
+                stream=sys.stdout,
+                format=(
+                    "%(levelname)-1.1s "  # Logging level
+                    "%(asctime)23.23s "  # Date and time
+                    "%(name)s: "  # Logger name
+                    "%(message)s"  # Message
+                ),
+            )
 
-    # DeprecationWarning is disabled by default, but it's useful to have enabled
-    warnings.simplefilter("always", DeprecationWarning)
-    logging.captureWarnings(True)
+    except Exception as ex:
+        raise RuntimeError("A problem occurred while trying to setup default logging configuration") from ex
 
 
 _UNCONDITIONAL_ANSI_FLAGS: typing.Final[typing.FrozenSet[str]] = frozenset(("PYCHARM_HOSTED", "WT_SESSION"))
@@ -179,6 +262,9 @@ def print_banner(
     .. note::
         The `banner.txt` must be in the root folder of the package.
 
+    .. warning::
+        This function is blocking!
+
     Parameters
     ----------
     package : typing.Optional[str]
@@ -237,9 +323,9 @@ def print_banner(
         for code in colorlog.escape_codes.escape_codes:
             args[code] = ""
 
-    with open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as stdout:
-        stdout.write(string.Template(raw_banner).safe_substitute(args))
-        stdout.flush()
+    banner_str = string.Template(raw_banner).safe_substitute(args)
+    sys.stdout.buffer.write(banner_str.encode("utf-8"))
+    sys.stdout.flush()
 
 
 def warn_if_not_optimized(suppress: bool) -> None:
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/events/test_guild_events.py b/tests/hikari/events/test_guild_events.py
index 35bed5e7..5e50c72b 100644
--- a/tests/hikari/events/test_guild_events.py
+++ b/tests/hikari/events/test_guild_events.py
@@ -179,3 +179,15 @@ async def test_fetch_stickers(self, event):
         assert await event.fetch_stickers() is event.app.rest.fetch_guild_stickers.return_value
 
         event.app.rest.fetch_guild_stickers.assert_awaited_once_with(event.guild_id)
+
+
+class TestAuditLogEntryCreateEvent:
+    @pytest.fixture()
+    def event(self):
+        return guild_events.AuditLogEntryCreateEvent(shard=mock.Mock(), entry=mock.Mock())
+
+    def test_app_property(self, event):
+        assert event.app is event.entry.app
+
+    def test_guild_id_property(self, event):
+        assert event.guild_id is event.entry.guild_id
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 3929e750..8c48f291 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -1350,45 +1350,11 @@ def partial_integration_payload(self):
             "account": {"id": "543453", "name": "Blam"},
         }
 
-    @pytest.fixture()
-    def audit_log_payload(
-        self,
-        audit_log_entry_payload,
-        user_payload,
-        incoming_webhook_payload,
-        application_webhook_payload,
-        follower_webhook_payload,
-        partial_integration_payload,
-        guild_public_thread_payload,
-        guild_private_thread_payload,
-        guild_news_thread_payload,
-    ):
-        return {
-            "audit_log_entries": [audit_log_entry_payload],
-            "integrations": [partial_integration_payload],
-            "threads": [guild_public_thread_payload, guild_private_thread_payload, guild_news_thread_payload],
-            "users": [user_payload],
-            "webhooks": [incoming_webhook_payload, application_webhook_payload, follower_webhook_payload],
-        }
-
-    def test_deserialize_audit_log(
-        self,
-        entity_factory_impl,
-        mock_app,
-        audit_log_payload,
-        user_payload,
-        incoming_webhook_payload,
-        application_webhook_payload,
-        follower_webhook_payload,
-        partial_integration_payload,
-        guild_public_thread_payload,
-        guild_private_thread_payload,
-        guild_news_thread_payload,
-    ):
-        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload)
+    def test_deserialize_audit_log_entry(self, entity_factory_impl, audit_log_entry_payload, mock_app):
+        entry = entity_factory_impl.deserialize_audit_log_entry(
+            audit_log_entry_payload, guild_id=snowflakes.Snowflake(123321)
+        )
 
-        assert len(audit_log.entries) == 1
-        entry = audit_log.entries[694026906592477214]
         assert entry.app is mock_app
         assert entry.id == 694026906592477214
         assert entry.target_id == 115590097100865541
@@ -1397,6 +1363,7 @@ def test_deserialize_audit_log(
         assert entry.options.id == 115590097100865541
         assert entry.options.type == channel_models.PermissionOverwriteType.MEMBER
         assert entry.options.role_name is None
+        assert entry.guild_id == 123321
         assert entry.reason == "An artificial insanity."
 
         assert len(entry.changes) == 1
@@ -1415,34 +1382,30 @@ def test_deserialize_audit_log(
         role.id == 123123123312312
         role.name == "aRole"
 
-        assert audit_log.integrations == {
-            4949494949: entity_factory_impl.deserialize_partial_integration(partial_integration_payload)
-        }
-        assert audit_log.threads == {
-            947643783913308301: entity_factory_impl.deserialize_guild_public_thread(guild_public_thread_payload),
-            947690637610844210: entity_factory_impl.deserialize_guild_private_thread(guild_private_thread_payload),
-            946900871160164393: entity_factory_impl.deserialize_guild_news_thread(guild_news_thread_payload),
-        }
-        assert audit_log.users == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)}
-        assert audit_log.webhooks == {
-            223704706495545344: entity_factory_impl.deserialize_incoming_webhook(incoming_webhook_payload),
-            658822586720976555: entity_factory_impl.deserialize_application_webhook(application_webhook_payload),
-            752831914402115456: entity_factory_impl.deserialize_channel_follower_webhook(follower_webhook_payload),
-        }
+    def test_deserialize_audit_log_entry_when_guild_id_in_payload(
+        self, entity_factory_impl, audit_log_entry_payload, mock_app
+    ):
+        audit_log_entry_payload["guild_id"] = 431123123
+
+        entry = entity_factory_impl.deserialize_audit_log_entry(audit_log_entry_payload)
+
+        assert entry.guild_id == 431123123
 
-    def test_deserialize_audit_log_with_unset_or_unknown_fields(self, entity_factory_impl, audit_log_payload):
+    def test_deserialize_audit_log_entry_with_unset_or_unknown_fields(
+        self, entity_factory_impl, audit_log_entry_payload
+    ):
         # Unset fields
-        audit_log_payload["audit_log_entries"][0]["changes"] = None
-        audit_log_payload["audit_log_entries"][0]["target_id"] = None
-        audit_log_payload["audit_log_entries"][0]["user_id"] = None
-        audit_log_payload["audit_log_entries"][0]["options"] = None
-        audit_log_payload["audit_log_entries"][0]["action_type"] = 69
-        del audit_log_payload["audit_log_entries"][0]["reason"]
+        audit_log_entry_payload["changes"] = None
+        audit_log_entry_payload["target_id"] = None
+        audit_log_entry_payload["user_id"] = None
+        audit_log_entry_payload["options"] = None
+        audit_log_entry_payload["action_type"] = 69
+        del audit_log_entry_payload["reason"]
 
-        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload)
+        entry = entity_factory_impl.deserialize_audit_log_entry(
+            audit_log_entry_payload, guild_id=snowflakes.Snowflake(1234321)
+        )
 
-        assert len(audit_log.entries) == 1
-        entry = audit_log.entries[694026906592477214]
         assert entry.changes == []
         assert entry.target_id is None
         assert entry.user_id is None
@@ -1450,40 +1413,109 @@ def test_deserialize_audit_log_with_unset_or_unknown_fields(self, entity_factory
         assert entry.options is None
         assert entry.reason is None
 
-    def test_deserialize_audit_log_with_unhandled_change_key(self, entity_factory_impl, audit_log_payload):
+    def test_deserialize_audit_log_entry_with_unhandled_change_key(self, entity_factory_impl, audit_log_entry_payload):
         # Unset fields
-        audit_log_payload["audit_log_entries"][0]["changes"][0]["key"] = "name"
+        audit_log_entry_payload["changes"][0]["key"] = "name"
 
-        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload)
+        entry = entity_factory_impl.deserialize_audit_log_entry(
+            audit_log_entry_payload, guild_id=snowflakes.Snowflake(4123123)
+        )
 
-        assert len(audit_log.entries) == 1
-        entry = audit_log.entries[694026906592477214]
         assert len(entry.changes) == 1
         change = entry.changes[0]
         assert change.key == audit_log_models.AuditLogChangeKey.NAME
         assert change.new_value == [{"id": "568651298858074123", "name": "Casual"}]
         assert change.old_value == [{"id": "123123123312312", "name": "aRole"}]
 
-    def test_deserialize_audit_log_with_change_key_unknown(self, entity_factory_impl, audit_log_payload):
+    def test_deserialize_audit_log_entry_with_change_key_unknown(self, entity_factory_impl, audit_log_entry_payload):
         # Unset fields
-        audit_log_payload["audit_log_entries"][0]["changes"][0]["key"] = "unknown"
+        audit_log_entry_payload["changes"][0]["key"] = "unknown"
 
-        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload)
+        entry = entity_factory_impl.deserialize_audit_log_entry(
+            audit_log_entry_payload, guild_id=snowflakes.Snowflake(123312)
+        )
 
-        assert len(audit_log.entries) == 1
-        entry = audit_log.entries[694026906592477214]
         assert len(entry.changes) == 1
         change = entry.changes[0]
         assert change.key == "unknown"
         assert change.new_value == [{"id": "568651298858074123", "name": "Casual"}]
         assert change.old_value == [{"id": "123123123312312", "name": "aRole"}]
 
+    def test_deserialize_audit_log_entry_for_unknown_action_type(self, entity_factory_impl, audit_log_entry_payload):
+        # Unset fields
+        audit_log_entry_payload["action_type"] = 1000
+        audit_log_entry_payload["options"] = {"field1": "value1", "field2": 96}
+
+        with pytest.raises(errors.UnrecognisedEntityError):
+            entity_factory_impl.deserialize_audit_log_entry(
+                audit_log_entry_payload, guild_id=snowflakes.Snowflake(341123)
+            )
+
+    @pytest.fixture()
+    def audit_log_payload(
+        self,
+        audit_log_entry_payload,
+        user_payload,
+        incoming_webhook_payload,
+        application_webhook_payload,
+        follower_webhook_payload,
+        partial_integration_payload,
+        guild_public_thread_payload,
+        guild_private_thread_payload,
+        guild_news_thread_payload,
+    ):
+        return {
+            "audit_log_entries": [audit_log_entry_payload],
+            "integrations": [partial_integration_payload],
+            "threads": [guild_public_thread_payload, guild_private_thread_payload, guild_news_thread_payload],
+            "users": [user_payload],
+            "webhooks": [incoming_webhook_payload, application_webhook_payload, follower_webhook_payload],
+        }
+
+    def test_deserialize_audit_log(
+        self,
+        entity_factory_impl,
+        mock_app,
+        audit_log_payload,
+        audit_log_entry_payload,
+        user_payload,
+        incoming_webhook_payload,
+        application_webhook_payload,
+        follower_webhook_payload,
+        partial_integration_payload,
+        guild_public_thread_payload,
+        guild_private_thread_payload,
+        guild_news_thread_payload,
+    ):
+        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload, guild_id=snowflakes.Snowflake(123321))
+
+        assert audit_log.entries == {
+            694026906592477214: entity_factory_impl.deserialize_audit_log_entry(
+                audit_log_entry_payload, guild_id=snowflakes.Snowflake(123321)
+            )
+        }
+
+        assert audit_log.integrations == {
+            4949494949: entity_factory_impl.deserialize_partial_integration(partial_integration_payload)
+        }
+        assert audit_log.threads == {
+            947643783913308301: entity_factory_impl.deserialize_guild_public_thread(guild_public_thread_payload),
+            947690637610844210: entity_factory_impl.deserialize_guild_private_thread(guild_private_thread_payload),
+            946900871160164393: entity_factory_impl.deserialize_guild_news_thread(guild_news_thread_payload),
+        }
+        assert audit_log.users == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)}
+        assert audit_log.webhooks == {
+            223704706495545344: entity_factory_impl.deserialize_incoming_webhook(incoming_webhook_payload),
+            658822586720976555: entity_factory_impl.deserialize_application_webhook(application_webhook_payload),
+            752831914402115456: entity_factory_impl.deserialize_channel_follower_webhook(follower_webhook_payload),
+        }
+
     def test_deserialize_audit_log_with_action_type_unknown_gets_ignored(self, entity_factory_impl, audit_log_payload):
         # Unset fields
         audit_log_payload["audit_log_entries"][0]["action_type"] = 1000
         audit_log_payload["audit_log_entries"][0]["options"] = {"field1": "value1", "field2": 96}
 
-        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload)
+        audit_log = entity_factory_impl.deserialize_audit_log(audit_log_payload, guild_id=snowflakes.Snowflake(341123))
 
         assert len(audit_log.entries) == 0
 
@@ -1500,7 +1532,8 @@ def test_deserialize_audit_log_skips_unknown_webhook_type(
                 "users": [],
                 "audit_log_entries": [],
                 "integrations": [],
-            }
+            },
+            guild_id=snowflakes.Snowflake(53123123),
         )
 
         assert audit_log.webhooks == {
@@ -1521,7 +1554,8 @@ def test_deserialize_audit_log_skips_unknown_thread_type(
                 "users": [],
                 "audit_log_entries": [],
                 "integrations": [],
-            }
+            },
+            guild_id=snowflakes.Snowflake(5412123),
         )
 
         assert audit_log.threads == {
@@ -6870,24 +6904,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)
diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py
index 60735d7a..01e6c8e0 100644
--- a/tests/hikari/impl/test_event_factory.py
+++ b/tests/hikari/impl/test_event_factory.py
@@ -746,6 +746,18 @@ def test_deserialize_presence_update_event_with_partial_user_object(self, event_
 
         assert event.presence is mock_app.entity_factory.deserialize_member_presence.return_value
 
+    def test_deserialize_audit_log_entry_create_event(
+        self, event_factory: event_factory_.EventFactoryImpl, mock_app, mock_shard
+    ):
+        payload = {"id": "439034093490"}
+
+        result = event_factory.deserialize_audit_log_entry_create_event(mock_shard, payload)
+
+        mock_app.entity_factory.deserialize_audit_log_entry.assert_called_once_with(payload)
+        assert result.entry is mock_app.entity_factory.deserialize_audit_log_entry.return_value
+        assert result.shard is mock_shard
+        assert isinstance(result, guild_events.AuditLogEntryCreateEvent)
+
     ######################
     # INTERACTION EVENTS #
     ######################
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index 02e1d615..5d1514d4 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -1690,3 +1690,19 @@ async def test_on_guild_scheduled_event_user_remove(
         event_manager_impl.dispatch.assert_awaited_once_with(
             event_factory.deserialize_scheduled_event_user_remove_event.return_value
         )
+
+    @pytest.mark.asyncio()
+    async def test_on_guild_audit_log_entry_create(
+        self,
+        event_manager_impl: event_manager.EventManagerImpl,
+        shard: mock.Mock,
+        event_factory: event_factory_.EventFactory,
+    ):
+        mock_payload = mock.Mock()
+
+        await event_manager_impl.on_guild_audit_log_entry_create(shard, mock_payload)
+
+        event_factory.deserialize_audit_log_entry_create_event.assert_called_once_with(shard, mock_payload)
+        event_manager_impl.dispatch.assert_awaited_once_with(
+            event_factory.deserialize_audit_log_entry_create_event.return_value
+        )
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index d5cd6828..38f04c91 100644
--- a/tests/hikari/internal/test_ux.py
+++ b/tests/hikari/internal/test_ux.py
@@ -19,11 +19,12 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-import builtins
 import contextlib
 import importlib
 import logging
+import logging.config
 import os
+import pathlib
 import platform
 import string
 import sys
@@ -46,103 +47,169 @@ def test_when_handlers_already_set_up(self):
         stack.enter_context(mock.patch.object(logging, "root", handlers=[None]))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
         logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
 
         with stack:
             ux.init_logging("LOGGING_LEVEL", True, False)
 
         logging_dict_config.assert_not_called()
         logging_basic_config.assert_not_called()
-        colorlog_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
 
     def test_when_handlers_specify_not_to_set_up(self):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
         logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
 
         with stack:
             ux.init_logging(None, True, False)
 
         logging_dict_config.assert_not_called()
         logging_basic_config.assert_not_called()
-        colorlog_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
 
-    def test_when_flavour_is_a_dict_and_doesnt_define_handlers(self):
+    def test_when_flavour_is_a_dict_and_is_not_incremental(self):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
         stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
         logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
 
         with stack:
             ux.init_logging({"hikari": {"level": "INFO"}}, True, False)
 
+        logging_file_config.assert_not_called()
         logging_dict_config.assert_called_once_with({"hikari": {"level": "INFO"}})
-        logging_basic_config.assert_called_once_with(
-            level=None,
-            format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s",
-            stream=sys.stderr,
-        )
-        colorlog_basic_config.assert_not_called()
+        logging_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
+
+    def test_when_flavour_is_a_dict_and_is_incremental(self):
+        # This will emulate it being populated during the basicConfig call
+        def _basicConfig(*args, **kwargs):
+            logging_basic_config(*args, **kwargs)
+            stack.enter_context(mock.patch.object(logging.root, "handlers", new=[handler]))
 
-    def test_when_flavour_is_a_dict_and_defines_handlers(self):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
+        stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
-        logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
+        handler = mock.Mock()
+        logging_basic_config = mock.Mock()
+        stack.enter_context(mock.patch.object(logging, "basicConfig", new=_basicConfig))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
 
         with stack:
-            ux.init_logging({"hikari": {"level": "INFO"}, "handlers": {"some_handler": {}}}, True, False)
+            ux.init_logging({"incremental": True, "hikari": {"level": "INFO"}}, True, False)
 
-        logging_dict_config.assert_called_once_with({"hikari": {"level": "INFO"}, "handlers": {"some_handler": {}}})
-        logging_basic_config.assert_not_called()
-        colorlog_basic_config.assert_not_called()
+        logging_file_config.assert_not_called()
+        logging_dict_config.assert_called_once_with({"incremental": True, "hikari": {"level": "INFO"}})
+        logging_basic_config.assert_called_once_with(
+            level=None,
+            stream=sys.stdout,
+            format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s",
+        )
+        colored_formatter.assert_not_called()
+        handler.setFormatter.assert_not_called()
 
     def test_when_supports_color(self):
+        # This will emulate it being populated during the basicConfig call
+        def _basicConfig(*args, **kwargs):
+            logging_basic_config(*args, **kwargs)
+            stack.enter_context(mock.patch.object(logging.root, "handlers", new=[handler]))
+
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
-        logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
+        handler = mock.Mock()
+        logging_basic_config = mock.Mock()
+        stack.enter_context(mock.patch.object(logging, "basicConfig", new=_basicConfig))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
         supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=True))
 
         with stack:
             ux.init_logging("LOGGING_LEVEL", True, False)
 
+        logging_file_config.assert_not_called()
         logging_dict_config.assert_not_called()
-        logging_basic_config.assert_not_called()
-        colorlog_basic_config.assert_called_once_with(
-            level="LOGGING_LEVEL",
-            format="%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: "
+        logging_basic_config.assert_called_once_with(level="LOGGING_LEVEL", stream=sys.stdout)
+        colored_formatter.assert_called_once_with(
+            fmt="%(log_color)s%(bold)s%(levelname)-1.1s%(thin)s %(asctime)23.23s %(bold)s%(name)s: "
             "%(thin)s%(message)s%(reset)s",
-            stream=sys.stderr,
+            force_color=True,
         )
         supports_color.assert_called_once_with(True, False)
+        handler.setFormatter.assert_called_once_with(colored_formatter.return_value)
 
     def test_when_doesnt_support_color(self):
         stack = contextlib.ExitStack()
         stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
         logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
         logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
-        colorlog_basic_config = stack.enter_context(mock.patch.object(colorlog, "basicConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
         supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
 
         with stack:
             ux.init_logging("LOGGING_LEVEL", True, False)
 
         logging_dict_config.assert_not_called()
+        logging_file_config.assert_not_called()
         logging_basic_config.assert_called_once_with(
             level="LOGGING_LEVEL",
             format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s",
-            stream=sys.stderr,
+            stream=sys.stdout,
         )
-        colorlog_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
         supports_color.assert_called_once_with(True, False)
 
+    def test_when_flavour_is_pathlike(self):
+        path = pathlib.Path("some/path/to/somewhere.ini")
+
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
+        pathlib_path = stack.enter_context(mock.patch.object(pathlib, "Path"))
+        logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
+        logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
+        supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
+
+        with stack:
+            ux.init_logging(path, True, False)
+
+        pathlib_path.assert_not_called()
+        logging_dict_config.assert_not_called()
+        logging_file_config.assert_called_once_with(path)
+        logging_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
+        supports_color.assert_not_called()
+
+    def test_when_flavour_is_an_existing_path(self):
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(logging, "root", handlers=[]))
+        pathlib_path = stack.enter_context(mock.patch.object(pathlib, "Path"))
+        logging_dict_config = stack.enter_context(mock.patch.object(logging.config, "dictConfig"))
+        logging_basic_config = stack.enter_context(mock.patch.object(logging, "basicConfig"))
+        logging_file_config = stack.enter_context(mock.patch.object(logging.config, "fileConfig"))
+        colored_formatter = stack.enter_context(mock.patch.object(colorlog.formatter, "ColoredFormatter"))
+        supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
+
+        with stack:
+            ux.init_logging("some/path/to/somewhere.ini", True, False)
+
+        pathlib_path.assert_called_once_with("some/path/to/somewhere.ini")
+        logging_dict_config.assert_not_called()
+        logging_file_config.assert_called_once_with(pathlib_path.return_value)
+        logging_basic_config.assert_not_called()
+        colored_formatter.assert_not_called()
+        supports_color.assert_not_called()
+
 
 class TestRedBanner:
     def test_when_above_3_9(self):
@@ -233,10 +300,9 @@ def test_when_supports_color(self, mock_args):
         supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=True))
         read_banner = stack.enter_context(mock.patch.object(ux, "_read_banner"))
         template = stack.enter_context(mock.patch.object(string, "Template"))
-        builtins_open = stack.enter_context(mock.patch.object(builtins, "open"))
         abspath = stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
         dirname = stack.enter_context(mock.patch.object(os.path, "dirname"))
-        fileno = stack.enter_context(mock.patch.object(sys.stdout, "fileno"))
+        stdout = stack.enter_context(mock.patch.object(sys, "stdout"))
 
         with stack:
             ux.print_banner("hikaru", True, False)
@@ -261,12 +327,13 @@ def test_when_supports_color(self, mock_args):
 
         read_banner.assert_called_once_with("hikaru")
         template.assert_called_once_with(read_banner.return_value)
-        template().safe_substitute.assert_called_once_with(args)
-        builtins_open.assert_called_once_with(fileno.return_value, "w", encoding="utf-8", closefd=False)
-        builtins_open.return_value.__enter__.return_value.write.assert_called_once_with(template().safe_substitute())
+        safe_substitute = template.return_value.safe_substitute
+        safe_substitute.assert_called_once_with(args)
         dirname.assert_called_once_with("~/hikari")
         abspath.assert_called_once_with(dirname())
         supports_color.assert_called_once_with(True, False)
+        stdout.buffer.write.assert_called_once_with(safe_substitute.return_value.encode.return_value)
+        stdout.flush.assert_called_once_with()
 
     def test_when_doesnt_supports_color(self, mock_args):
         stack = contextlib.ExitStack()
@@ -279,8 +346,7 @@ def test_when_doesnt_supports_color(self, mock_args):
         template = stack.enter_context(mock.patch.object(string, "Template"))
         abspath = stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
         dirname = stack.enter_context(mock.patch.object(os.path, "dirname"))
-        builtins_open = stack.enter_context(mock.patch.object(builtins, "open"))
-        fileno = stack.enter_context(mock.patch.object(sys.stdout, "fileno"))
+        stdout = stack.enter_context(mock.patch.object(sys, "stdout"))
 
         with stack:
             ux.print_banner("hikaru", True, False)
@@ -304,12 +370,13 @@ def test_when_doesnt_supports_color(self, mock_args):
         }
 
         template.assert_called_once_with(read_banner.return_value)
-        template().safe_substitute.assert_called_once_with(args)
+        safe_substitute = template.return_value.safe_substitute
+        safe_substitute.assert_called_once_with(args)
         dirname.assert_called_once_with("~/hikari")
         abspath.assert_called_once_with(dirname())
         supports_color.assert_called_once_with(True, False)
-        builtins_open.assert_called_once_with(fileno.return_value, "w", encoding="utf-8", closefd=False)
-        builtins_open.return_value.__enter__.return_value.write.assert_called_once_with(template().safe_substitute())
+        stdout.buffer.write.assert_called_once_with(safe_substitute.return_value.encode.return_value)
+        stdout.flush.assert_called_once_with()
 
     def test_use_extra_args(self, mock_args):
         stack = contextlib.ExitStack()
@@ -317,9 +384,8 @@ def test_use_extra_args(self, mock_args):
         stack.enter_context(mock.patch.object(time, "sleep"))
         read_banner = stack.enter_context(mock.patch.object(ux, "_read_banner"))
         template = stack.enter_context(mock.patch.object(string, "Template"))
-        builtins_open = stack.enter_context(mock.patch.object(builtins, "open"))
         stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
-        fileno = stack.enter_context(mock.patch.object(sys.stdout, "fileno"))
+        stdout = stack.enter_context(mock.patch.object(sys, "stdout"))
 
         extra_args = {
             "extra_argument_1": "one",
@@ -348,9 +414,10 @@ def test_use_extra_args(self, mock_args):
 
         read_banner.assert_called_once_with("hikaru")
         template.assert_called_once_with(read_banner.return_value)
-        template().safe_substitute.assert_called_once_with(args)
-        builtins_open.assert_called_once_with(fileno.return_value, "w", encoding="utf-8", closefd=False)
-        builtins_open.return_value.__enter__.return_value.write.assert_called_once_with(template().safe_substitute())
+        safe_substitute = template.return_value.safe_substitute
+        safe_substitute.assert_called_once_with(args)
+        stdout.buffer.write.assert_called_once_with(safe_substitute.return_value.encode.return_value)
+        stdout.flush.assert_called_once_with()
 
     def test_overwrite_args_raises_error(self, mock_args):
         stack = contextlib.ExitStack()
diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py
index aeaf50d3..2b3eac58 100644
--- a/tests/hikari/test_audit_logs.py
+++ b/tests/hikari/test_audit_logs.py
@@ -82,6 +82,7 @@ async def test_fetch_user_when_no_user(self):
             action_type=0,
             options=None,
             reason=None,
+            guild_id=snowflakes.Snowflake(34123123),
         )
 
         assert await model.fetch_user() is None
@@ -99,6 +100,7 @@ async def test_fetch_user_when_user(self):
             action_type=0,
             options=None,
             reason=None,
+            guild_id=snowflakes.Snowflake(123321123),
         )
 
         assert await model.fetch_user() is model.app.rest.fetch_user.return_value
diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py
index f481a9b0..99c62cb5 100644
--- a/tests/hikari/test_errors.py
+++ b/tests/hikari/test_errors.py
@@ -249,7 +249,7 @@ def test_str(self, error):
 class TestMissingIntentError:
     @pytest.fixture()
     def error(self):
-        return errors.MissingIntentError(intents.Intents.GUILD_BANS | intents.Intents.GUILD_EMOJIS)
+        return errors.MissingIntentError(intents.Intents.GUILD_MEMBERS | intents.Intents.GUILD_EMOJIS)
 
     def test_str(self, error):
-        assert str(error) == "You are missing the following intent(s): GUILD_BANS, GUILD_EMOJIS"
+        assert str(error) == "You are missing the following intent(s): GUILD_EMOJIS, GUILD_MEMBERS"