Repository: hikari-py/hikari
Pull request #392 (2020-12-15 02:20:01)

Add endpoints and objects for handling templates.
### Summary
Add endpoints and objects for handling templates.

### 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)
-->



Patch:
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 1ed0c783..f7de74c4 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -6,7 +6,7 @@
 mock==4.0.3
 
 # Py.test stuff.
-pytest==6.2.0
+pytest==6.2.1
 pytest-asyncio==0.14.0
 pytest-cov==2.10.1
 pytest-randomly==3.5.0
diff --git a/hikari/__init__.py b/hikari/__init__.py
index 0060fda4..1a0ca05e 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -72,6 +72,7 @@
 from hikari.snowflakes import Snowflakeish
 from hikari.snowflakes import SnowflakeishOr
 from hikari.snowflakes import Unique
+from hikari.templates import *
 from hikari.traits import *
 from hikari.undefined import UNDEFINED
 from hikari.undefined import UndefinedNoneOr
diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py
index 05bb4f0b..5a2d9335 100644
--- a/hikari/api/entity_factory.py
+++ b/hikari/api/entity_factory.py
@@ -45,6 +45,7 @@
     from hikari import presences as presence_models
     from hikari import sessions as gateway_models
     from hikari import snowflakes
+    from hikari import templates as template_models
     from hikari import users as user_models
     from hikari import voices as voice_models
     from hikari import webhooks as webhook_models
@@ -941,6 +942,25 @@ def deserialize_member_presence(
             If this is raised, no guild ID info was provided anywhere.
         """
 
+    ###################
+    # TEMPLATE MODELS #
+    ###################
+
+    @abc.abstractmethod
+    def deserialize_template(self, payload: data_binding.JSONObject) -> template_models.Template:
+        """Parse a raw payload from Discord into a template object.
+
+        Parameters
+        ----------
+        payload : hikari.internal.data_binding.JSONObject
+            The JSON payload to deserialize.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The deserialized template object.
+        """
+
     ###############
     # USER MODELS #
     ###############
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 77cf69b4..b8bdee1a 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -48,6 +48,7 @@
     from hikari import permissions as permissions_
     from hikari import sessions
     from hikari import snowflakes
+    from hikari import templates
     from hikari import users
     from hikari import voices
     from hikari import webhooks
@@ -1597,7 +1598,7 @@ async def create_webhook(
         channel: snowflakes.SnowflakeishOr[channels.TextChannel],
         name: str,
         *,
-        avatar: typing.Optional[files.Resourceish] = None,
+        avatar: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> webhooks.Webhook:
         """Create webhook in a channel.
@@ -2517,7 +2518,6 @@ async def leave_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild
             If an internal error occurs on Discord while handling the request.
         """
 
-    # THIS IS AN OAUTH2 FLOW ONLY
     @abc.abstractmethod
     async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialUser], /) -> channels.DMChannel:
         """Create a DM channel with a user.
@@ -3063,10 +3063,14 @@ def guild_builder(self, name: str, /) -> special_endpoints.GuildBuilder:
             The guild builder to use. This will allow to create a guild
             later with `hikari.api.special_endpoints.GuildBuilder.create`.
 
+        !!! note
+            This endpoint can only be used by bots in less than 10 guilds.
+
         Raises
         ------
         hikari.errors.BadRequestError
-            If any of the fields that are passed have an invalid value.
+            If any of the fields that are passed have an invalid value or if you
+            call this as a bot that's in more than 10 guilds.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.RateLimitTooLongError
@@ -4960,3 +4964,324 @@ async def fetch_vanity_url(self, guild: snowflakes.SnowflakeishOr[guilds.Partial
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """
+
+    @abc.abstractmethod
+    async def create_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        name: str,
+        *,
+        description: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED,
+    ) -> templates.Template:
+        """Create a guild template.
+
+        Parameters
+        ----------
+        guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]
+            The guild to create a template from.
+        name : str
+            The name to use for the created template.
+
+        Other Parameters
+        ----------------
+        description : hikari.undefined.UndefinedNoneOr[builtins.str]
+            The description to set for the template.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The object of the created template.
+
+        Raises
+        ------
+        hikari.errors.ForbiddenError
+            If you are not part of the guild.
+        hikari.errors.NotFoundError
+            If the guild is not found or you are missing the `MANAGE_GUILD`
+            permission.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def create_guild_from_template(
+        self,
+        template: templates.Templateish,
+        name: str,
+        *,
+        icon: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
+    ) -> guilds.RESTGuild:
+        """Make a guild from a template.
+
+        Parameters
+        ----------
+        template: hikari.templates.Templateish
+            The objecr or code of the template to create a guild based on.
+        name : builtins.str
+            The new guilds name.
+
+        Other Parameters
+        ----------------
+        icon : hikari.undefined.UndefinedOr[hikari.files.Resourceish]
+            If provided, the guild icon to set. Must be a 1024x1024 image or can
+            be an animated gif when the guild has the `ANIMATED_ICON` feature.
+
+        Returns
+        -------
+        hikari.guilds.RESTGuild
+            Object of the created guild.
+
+        !!! note
+            This endpoint can only be used by bots in less than 10 guilds.
+
+        Raises
+        ------
+        hikari.errors.BadRequestError
+            If any of the fields that are passed have an invalid value or if you
+            call this as a bot that's in more than 10 guilds.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def delete_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+    ) -> templates.Template:
+        """Delete a guild template.
+
+        Parameters
+        ----------
+        guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]
+            The guild to delete a template in.
+        template : hikari.templates.Templateish
+            Object or ID of the template to delete.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The deleted template's object.
+
+        Raises
+        ------
+        hikari.errors.ForbiddenError
+            If you are not part of the guild.
+        hikari.errors.NotFoundError
+            If the guild is not found or you are missing the `MANAGE_GUILD`
+            permission.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def edit_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+        *,
+        name: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        description: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED,
+    ) -> templates.Template:
+        """Modify a guild template.
+
+        Parameters
+        ----------
+        guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]
+            The guild to edit a template in.
+        template : hikari.templates.Templateish
+            Object or ID of the template to modify.
+
+        Other Parameters
+        ----------------
+        name : hikari.undefined.UndefinedOr[builtins.str]
+            The name to set for this template.
+        description : hikari.undefined.UndefinedNoneOr[builtins.str]
+            The description to set for the template.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The object of the edited template.
+
+        Raises
+        ------
+        hikari.errors.ForbiddenError
+            If you are not part of the guild.
+        hikari.errors.NotFoundError
+            If the guild is not found or you are missing the `MANAGE_GUILD`
+            permission.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def fetch_template(self, template: templates.Templateish) -> templates.Template:
+        """Fetch a guild template.
+
+        Parameters
+        ----------
+        template : hikari.templates.Templateish
+            The object or string code of the template to fetch.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The object of the found template.
+
+        Raises
+        ------
+        hikari.errors.NotFoundError
+            If the template was not found.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def fetch_guild_templates(
+        self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]
+    ) -> typing.Sequence[templates.Template]:
+        """Fetch the templates for a guild.
+
+        Parameters
+        ----------
+        guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]
+            The object or ID of the guild to get the templates for.
+
+        Returns
+        -------
+        typing.Sequence[hikari.templates.Template]
+            A sequence of the found template objects.
+
+        Raises
+        ------
+        hikari.errors.ForbiddenError
+            If you are not part of the guild.
+        hikari.errors.NotFoundError
+            If the guild is not found or are missing the `MANAGE_GUILD`
+            permission.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def sync_guild_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+    ) -> templates.Template:
+        """Create a guild template.
+
+        Parameters
+        ----------
+        guild : hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]
+            The guild to sync a template in
+        template : hikari.templates.Templateish
+            Object or ID of the template to sync.
+
+        Returns
+        -------
+        hikari.templates.Template
+            The object of the synced template.
+
+        Raises
+        ------
+        hikari.errors.ForbiddenError
+            If you are not part of the guild or are missing the `MANAGE_GUILD`
+            permission.
+        hikari.errors.NotFoundError
+            If the guild or template is not found.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
diff --git a/hikari/applications.py b/hikari/applications.py
index 6e327551..f470a813 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -235,6 +235,9 @@ class OwnConnection:
 class OwnGuild(guilds.PartialGuild):
     """Represents a user bound partial guild object."""
 
+    features: typing.Sequence[guilds.GuildFeatureish] = attr.ib(eq=False, hash=False, repr=False)
+    """A list of the features in this guild."""
+
     is_owner: bool = attr.ib(eq=False, hash=False, repr=True)
     """`builtins.True` when the current user owns this guild."""
 
diff --git a/hikari/guilds.py b/hikari/guilds.py
index a3870912..d61ad06d 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -789,9 +789,6 @@ class PartialGuild(snowflakes.Unique):
     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."""
 
-    features: typing.Sequence[GuildFeatureish] = attr.ib(eq=False, hash=False, repr=False)
-    """A list of the features in this guild."""
-
     id: snowflakes.Snowflake = attr.ib(eq=True, hash=True, repr=True)
     """The ID of this entity."""
 
@@ -872,6 +869,9 @@ def format_icon(self, *, ext: typing.Optional[str] = None, size: int = 4096) ->
 class GuildPreview(PartialGuild):
     """A preview of a guild with the `GuildFeature.DISCOVERABLE` feature."""
 
+    features: typing.Sequence[GuildFeatureish] = attr.ib(eq=False, hash=False, repr=False)
+    """A list of the features in this guild."""
+
     splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
     """The hash of the splash for the guild, if there is one."""
 
@@ -971,6 +971,9 @@ def format_splash(self, *, ext: str = "png", size: int = 4096) -> typing.Optiona
 class Guild(PartialGuild, abc.ABC):
     """A representation of a guild on Discord."""
 
+    features: typing.Sequence[GuildFeatureish] = attr.ib(eq=False, hash=False, repr=False)
+    """A list of the features in this guild."""
+
     application_id: typing.Optional[snowflakes.Snowflake] = attr.ib(eq=False, hash=False, repr=False)
     """The ID of the application that created this guild.
 
@@ -1265,11 +1268,17 @@ class RESTGuild(Guild):
     _roles: typing.Mapping[snowflakes.Snowflake, Role] = attr.ib(eq=False, hash=False, repr=False)
     """The roles in this guild, represented as a mapping of role ID to role object."""
 
-    approximate_active_member_count: int = attr.ib(eq=False, hash=False, repr=False)
-    """The approximate number of members in the guild that are not offline."""
+    approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False)
+    """The approximate number of members in the guild that are not offline.
+
+    This will be `builtins.None` when creating a guild.
+    """
+
+    approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False)
+    """The approximate number of members in the guild.
 
-    approximate_member_count: int = attr.ib(eq=False, hash=False, repr=False)
-    """The approximate number of members in the guild."""
+    This will be `builtins.None` when creating a guild.
+    """
 
     max_presences: int = attr.ib(eq=False, hash=False, repr=False)
     """The maximum number of presences for the guild."""
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 7f16ff27..10535829 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -44,6 +44,7 @@
 from hikari import presences as presence_models
 from hikari import sessions as gateway_models
 from hikari import snowflakes
+from hikari import templates as template_models
 from hikari import traits
 from hikari import undefined
 from hikari import users as user_models
@@ -73,15 +74,6 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]:
     return datetime.timedelta(seconds=seconds) if seconds > 0 else None
 
 
-@attr_extensions.with_copy
-@attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False)
-class _PartialGuildFields:
-    id: snowflakes.Snowflake = attr.ib()
-    name: str = attr.ib()
-    icon_hash: str = attr.ib()
-    features: typing.Sequence[guild_models.GuildFeatureish] = attr.ib()
-
-
 @attr_extensions.with_copy
 @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False)
 class _GuildChannelFields:
@@ -106,7 +98,11 @@ class _IntegrationFields:
 
 @attr_extensions.with_copy
 @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False)
-class _GuildFields(_PartialGuildFields):
+class _GuildFields:
+    id: snowflakes.Snowflake = attr.ib()
+    name: str = attr.ib()
+    icon_hash: str = attr.ib()
+    features: typing.Sequence[guild_models.GuildFeatureish] = attr.ib()
     splash_hash: typing.Optional[str] = attr.ib()
     discovery_splash_hash: typing.Optional[str] = attr.ib()
     owner_id: snowflakes.Snowflake = attr.ib()
@@ -259,13 +255,12 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic
         )
 
     def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application_models.OwnGuild:
-        guild_fields = self._set_partial_guild_attributes(payload)
         return application_models.OwnGuild(
             app=self._app,
-            id=guild_fields.id,
-            name=guild_fields.name,
-            icon_hash=guild_fields.icon_hash,
-            features=guild_fields.features,
+            id=snowflakes.Snowflake(payload["id"]),
+            name=payload["name"],
+            icon_hash=payload["icon"],
+            features=[guild_models.GuildFeature(feature) for feature in payload["features"]],
             is_owner=bool(payload["owner"]),
             my_permissions=permission_models.Permissions(int(payload["permissions"])),
         )
@@ -497,7 +492,7 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann
 
     def deserialize_dm(self, payload: data_binding.JSONObject) -> channel_models.DMChannel:
         last_message_id: typing.Optional[snowflakes.Snowflake] = None
-        if (raw_last_message_id := payload["last_message_id"]) is not None:
+        if (raw_last_message_id := payload.get("last_message_id")) is not None:
             last_message_id = snowflakes.Snowflake(raw_last_message_id)
 
         return channel_models.DMChannel(
@@ -511,7 +506,7 @@ def deserialize_dm(self, payload: data_binding.JSONObject) -> channel_models.DMC
 
     def deserialize_group_dm(self, payload: data_binding.JSONObject) -> channel_models.GroupDMChannel:
         last_message_id: typing.Optional[snowflakes.Snowflake] = None
-        if (raw_last_message_id := payload["last_message_id"]) is not None:
+        if (raw_last_message_id := payload.get("last_message_id")) is not None:
             last_message_id = snowflakes.Snowflake(raw_last_message_id)
 
         if (raw_nicks := payload.get("nicks")) is not None:
@@ -593,7 +588,7 @@ def deserialize_guild_text_channel(
         channel_fields = self._set_guild_channel_attributes(payload, guild_id=guild_id)
 
         last_message_id: typing.Optional[snowflakes.Snowflake] = None
-        if (raw_last_message_id := payload["last_message_id"]) is not None:
+        if (raw_last_message_id := payload.get("last_message_id")) is not None:
             last_message_id = snowflakes.Snowflake(raw_last_message_id)
 
         last_pin_timestamp: typing.Optional[datetime.datetime] = None
@@ -628,7 +623,7 @@ def deserialize_guild_news_channel(
         channel_fields = self._set_guild_channel_attributes(payload, guild_id=guild_id)
 
         last_message_id: typing.Optional[snowflakes.Snowflake] = None
-        if (raw_last_message_id := payload["last_message_id"]) is not None:
+        if (raw_last_message_id := payload.get("last_message_id")) is not None:
             last_message_id = snowflakes.Snowflake(raw_last_message_id)
 
         last_pin_timestamp: typing.Optional[datetime.datetime] = None
@@ -1132,31 +1127,18 @@ def deserialize_integration(
     def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guild_models.GuildMemberBan:
         return guild_models.GuildMemberBan(reason=payload["reason"], user=self.deserialize_user(payload["user"]))
 
-    @staticmethod
-    def _set_partial_guild_attributes(payload: data_binding.JSONObject) -> _PartialGuildFields:
-        features = []
-        for feature in payload["features"]:
-            features.append(guild_models.GuildFeature(feature))
-
-        return _PartialGuildFields(
-            id=snowflakes.Snowflake(payload["id"]),
-            name=payload["name"],
-            icon_hash=payload["icon"],
-            features=features,
-        )
-
     def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_models.GuildPreview:
-        guild_fields = self._set_partial_guild_attributes(payload)
+        guild_id = snowflakes.Snowflake(payload["id"])
         emojis = {
-            snowflakes.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji, guild_id=guild_fields.id)
+            snowflakes.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji, guild_id=guild_id)
             for emoji in payload["emojis"]
         }
         return guild_models.GuildPreview(
             app=self._app,
-            id=guild_fields.id,
-            name=guild_fields.name,
-            icon_hash=guild_fields.icon_hash,
-            features=guild_fields.features,
+            id=guild_id,
+            name=payload["name"],
+            icon_hash=payload["icon"],
+            features=[guild_models.GuildFeature(feature) for feature in payload["features"]],
             splash_hash=payload["splash"],
             discovery_splash_hash=payload["discovery_splash"],
             emojis=emojis,
@@ -1166,7 +1148,6 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_m
         )
 
     def _set_guild_attributes(self, payload: data_binding.JSONObject) -> _GuildFields:
-        guild_fields = self._set_partial_guild_attributes(payload)
         afk_channel_id = payload["afk_channel_id"]
         default_message_notifications = guild_models.GuildMessageNotificationsLevel(
             payload["default_message_notifications"]
@@ -1183,10 +1164,10 @@ def _set_guild_attributes(self, payload: data_binding.JSONObject) -> _GuildField
             snowflakes.Snowflake(public_updates_channel_id) if public_updates_channel_id is not None else None
         )
         return _GuildFields(
-            id=guild_fields.id,
-            name=guild_fields.name,
-            icon_hash=guild_fields.icon_hash,
-            features=guild_fields.features,
+            id=snowflakes.Snowflake(payload["id"]),
+            name=payload["name"],
+            icon_hash=payload["icon"],
+            features=[guild_models.GuildFeature(feature) for feature in payload["features"]],
             splash_hash=payload["splash"],
             # This is documented as always being present, but we have found old guilds where this is
             # not present. Quicker to just assume the documentation is wrong at this point than try
@@ -1218,8 +1199,15 @@ def _set_guild_attributes(self, payload: data_binding.JSONObject) -> _GuildField
 
     def deserialize_rest_guild(self, payload: data_binding.JSONObject) -> guild_models.RESTGuild:
         guild_fields = self._set_guild_attributes(payload)
-        approximate_member_count = int(payload["approximate_member_count"])
-        approximate_active_member_count = int(payload["approximate_presence_count"])
+
+        approximate_member_count: typing.Optional[int] = None
+        if "approximate_member_count" in payload:
+            approximate_member_count = int(payload["approximate_member_count"])
+
+        approximate_active_member_count: typing.Optional[int] = None
+        if "approximate_presence_count" in payload:
+            approximate_active_member_count = int(payload["approximate_presence_count"])
+
         max_members = int(payload["max_members"])
 
         raw_max_presences = payload["max_presences"]
@@ -1375,13 +1363,12 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject) -> _InviteFie
         guild_id: typing.Optional[snowflakes.Snowflake] = None
         if "guild" in payload:
             guild_payload = payload["guild"]
-            guild_fields = self._set_partial_guild_attributes(guild_payload)
             guild = invite_models.InviteGuild(
                 app=self._app,
-                id=guild_fields.id,
-                name=guild_fields.name,
-                features=guild_fields.features,
-                icon_hash=guild_fields.icon_hash,
+                id=snowflakes.Snowflake(guild_payload["id"]),
+                name=guild_payload["name"],
+                features=[guild_models.GuildFeature(feature) for feature in guild_payload["features"]],
+                icon_hash=guild_payload["icon"],
                 splash_hash=guild_payload["splash"],
                 banner_hash=guild_payload["banner"],
                 description=guild_payload["description"],
@@ -1878,6 +1865,77 @@ def deserialize_member_presence(  # noqa: CFQ001 - Max function length
             client_status=client_status,
         )
 
+    ###################
+    # TEMPLATE MODELS #
+    ###################
+
+    def deserialize_template(self, payload: data_binding.JSONObject) -> template_models.Template:
+        source_guild_payload = payload["serialized_source_guild"]
+        # For some reason the guild ID isn't on the actual guild object in this special case.
+        guild_id = snowflakes.Snowflake(payload["source_guild_id"])
+        default_message_notifications = guild_models.GuildMessageNotificationsLevel(
+            source_guild_payload["default_message_notifications"]
+        )
+        explicit_content_filter = guild_models.GuildExplicitContentFilterLevel(
+            source_guild_payload["explicit_content_filter"]
+        )
+
+        roles: typing.MutableMapping[snowflakes.Snowflake, template_models.TemplateRole] = {}
+        for role_payload in source_guild_payload["roles"]:
+            role = template_models.TemplateRole(
+                app=self._app,
+                id=snowflakes.Snowflake(role_payload["id"]),
+                name=role_payload["name"],
+                permissions=permission_models.Permissions(int(role_payload["permissions"])),
+                color=color_models.Color(role_payload["color"]),
+                is_hoisted=role_payload["hoist"],
+                is_mentionable=role_payload["mentionable"],
+            )
+            roles[role.id] = role
+
+        channels = {}
+        for channel_payload in source_guild_payload["channels"]:
+            channel = typing.cast(
+                "channel_models.GuildChannel", self.deserialize_channel(channel_payload, guild_id=guild_id)
+            )
+            channels[channel.id] = channel
+
+        afk_channel_id = source_guild_payload["afk_channel_id"]
+        system_channel_id = source_guild_payload["system_channel_id"]
+
+        source_guild = template_models.TemplateGuild(
+            app=self._app,
+            id=guild_id,
+            # For some reason in this case they use the key "icon_hash" rather than "icon".
+            # Cause Discord:TM:
+            icon_hash=source_guild_payload["icon_hash"],
+            name=source_guild_payload["name"],
+            description=source_guild_payload["description"],
+            region=source_guild_payload["region"],
+            verification_level=guild_models.GuildVerificationLevel(source_guild_payload["verification_level"]),
+            default_message_notifications=default_message_notifications,
+            explicit_content_filter=explicit_content_filter,
+            preferred_locale=source_guild_payload["preferred_locale"],
+            afk_timeout=datetime.timedelta(seconds=source_guild_payload["afk_timeout"]),
+            roles=roles,
+            channels=channels,
+            afk_channel_id=snowflakes.Snowflake(afk_channel_id) if afk_channel_id is not None else None,
+            system_channel_id=snowflakes.Snowflake(system_channel_id) if system_channel_id is not None else None,
+            system_channel_flags=guild_models.GuildSystemChannelFlag(source_guild_payload["system_channel_flags"]),
+        )
+
+        return template_models.Template(
+            code=payload["code"],
+            name=payload["name"],
+            description=payload["description"],
+            usage_count=payload["usage_count"],
+            creator=self.deserialize_user(payload["creator"]),
+            created_at=time.iso8601_datetime_string_to_datetime(payload["created_at"]),
+            updated_at=time.iso8601_datetime_string_to_datetime(payload["updated_at"]),
+            source_guild=source_guild,
+            is_dirty=payload["is_dirty"],
+        )
+
     ###############
     # USER MODELS #
     ###############
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index b8224a97..8f86c2a6 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -87,6 +87,7 @@
     from hikari import invites
     from hikari import messages as messages_
     from hikari import sessions
+    from hikari import templates
     from hikari import voices
     from hikari import webhooks
     from hikari.api import entity_factory as entity_factory_
@@ -1280,14 +1281,14 @@ async def create_webhook(
         channel: snowflakes.SnowflakeishOr[channels.TextChannel],
         name: str,
         *,
-        avatar: typing.Optional[files.Resourceish] = None,
+        avatar: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> webhooks.Webhook:
         route = routes.POST_CHANNEL_WEBHOOKS.compile(channel=channel)
         body = data_binding.JSONObjectBuilder()
         body.put("name", name)
 
-        if avatar is not None:
+        if avatar is not undefined.UNDEFINED:
             avatar_resource = files.ensure_resource(avatar)
             async with avatar_resource.stream(executor=self._executor) as stream:
                 body.put("avatar", await stream.data_uri())
@@ -2399,3 +2400,94 @@ async def fetch_vanity_url(self, guild: snowflakes.SnowflakeishOr[guilds.Partial
         raw_response = await self._request(route)
         response = typing.cast(data_binding.JSONObject, raw_response)
         return self._entity_factory.deserialize_vanity_url(response)
+
+    async def create_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        name: str,
+        *,
+        description: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED,
+    ) -> templates.Template:
+        route = routes.POST_GUILD_TEMPLATES.compile(guild=guild)
+        body = data_binding.JSONObjectBuilder()
+        body.put("name", name)
+        body.put("description", description)
+        raw_response = await self._request(route, json=body)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_template(response)
+
+    async def create_guild_from_template(
+        self,
+        template: templates.Templateish,
+        name: str,
+        *,
+        icon: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
+    ) -> guilds.RESTGuild:
+        template = template if isinstance(template, str) else template.code
+        route = routes.POST_TEMPLATE.compile(template=template)
+        body = data_binding.JSONObjectBuilder()
+        body.put("name", name)
+
+        if icon is not undefined.UNDEFINED:
+            icon_resource = files.ensure_resource(icon)
+            async with icon_resource.stream(executor=self._executor) as stream:
+                body.put("icon", await stream.data_uri())
+
+        raw_response = await self._request(route, json=body)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_rest_guild(response)
+
+    async def delete_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+    ) -> templates.Template:
+        template = template if isinstance(template, str) else template.code
+        route = routes.DELETE_GUILD_TEMPLATE.compile(guild=guild, template=template)
+        raw_response = await self._request(route)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_template(response)
+
+    async def edit_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+        *,
+        name: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        description: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED,
+    ) -> templates.Template:
+        template = template if isinstance(template, str) else template.code
+        route = routes.PATCH_GUILD_TEMPLATE.compile(guild=guild, template=template)
+        body = data_binding.JSONObjectBuilder()
+        body.put("name", name)
+        body.put("description", description)
+
+        raw_response = await self._request(route, json=body)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_template(response)
+
+    async def fetch_template(self, template: templates.Templateish) -> templates.Template:
+        template = template if isinstance(template, str) else template.code
+        route = routes.GET_TEMPLATE.compile(template=template)
+        raw_response = await self._request(route)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_template(response)
+
+    async def fetch_guild_templates(
+        self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]
+    ) -> typing.Sequence[templates.Template]:
+        route = routes.GET_GUILD_TEMPLATES.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_template)
+
+    async def sync_guild_template(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        template: templates.Templateish,
+    ) -> templates.Template:
+        template = template if isinstance(template, str) else template.code
+        route = routes.PUT_GUILD_TEMPLATE.compile(guild=guild, template=template)
+        raw_response = await self._request(route)
+        response = typing.cast(data_binding.JSONObject, raw_response)
+        return self._entity_factory.deserialize_template(response)
diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py
index 3bc2d715..7bc0bea6 100644
--- a/hikari/internal/routes.py
+++ b/hikari/internal/routes.py
@@ -392,6 +392,15 @@ def compile_to_file(
 
 GET_GUILD_BANNER_IMAGE: typing.Final[Route] = Route(GET, "/guilds/{guild}/widget.png")
 
+# Templates
+DELETE_GUILD_TEMPLATE: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/templates/{template}")
+GET_TEMPLATE: typing.Final[Route] = Route(GET, "/guilds/templates/{template}")
+GET_GUILD_TEMPLATES: typing.Final[Route] = Route(GET, "/guilds/{guild}/templates")
+PATCH_GUILD_TEMPLATE: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/templates/{template}")
+POST_GUILD_TEMPLATES: typing.Final[Route] = Route(POST, "/guilds/{guild}/templates")
+POST_TEMPLATE: typing.Final[Route] = Route(POST, "/guilds/templates/{template}")
+PUT_GUILD_TEMPLATE: typing.Final[Route] = Route(PUT, "/guilds/{guild}/templates/{template}")
+
 # Invites
 GET_INVITE: typing.Final[Route] = Route(GET, "/invites/{invite_code}")
 DELETE_INVITE: typing.Final[Route] = Route(DELETE, "/invites/{invite_code}")
diff --git a/hikari/invites.py b/hikari/invites.py
index 79d62c79..b6422df5 100644
--- a/hikari/invites.py
+++ b/hikari/invites.py
@@ -100,6 +100,9 @@ def __str__(self) -> str:
 class InviteGuild(guilds.PartialGuild):
     """Represents the partial data of a guild that is attached to invites."""
 
+    features: typing.Sequence[guilds.GuildFeatureish] = attr.ib(eq=False, hash=False, repr=False)
+    """A list of the features in this guild."""
+
     splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
     """The hash of the splash for the guild, if there is one."""
 
diff --git a/hikari/templates.py b/hikari/templates.py
new file mode 100644
index 00000000..78efa3b6
--- /dev/null
+++ b/hikari/templates.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+# cython: language_level=3
+# Copyright (c) 2020 Nekokatt
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# 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.
+"""Application and entities that are used to describe guild templates on Discord."""
+
+from __future__ import annotations
+
+__all__: typing.List[str] = ["Template", "TemplateGuild", "TemplateRole", "Templateish"]
+
+import typing
+
+import attr
+
+from hikari import guilds
+from hikari.internal import attr_extensions
+
+if typing.TYPE_CHECKING:
+    import datetime
+
+    from hikari import channels as channels_
+    from hikari import colors
+    from hikari import permissions as permissions_
+    from hikari import snowflakes
+    from hikari import users
+
+
+@attr_extensions.with_copy
+@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
+class TemplateRole(guilds.PartialRole):
+    """The partial role object attached to `Template`."""
+
+    permissions: permissions_.Permissions = attr.ib(eq=False, hash=False, repr=False)
+    """The guild wide permissions this role gives to the members it's attached to,
+
+    This may be overridden by channel overwrites.
+    """
+
+    color: colors.Color = attr.ib(eq=False, hash=False, repr=True)
+    """The colour of this role.
+
+    This will be applied to a member's name in chat if it's their top coloured role.
+    """
+
+    is_hoisted: bool = attr.ib(eq=False, hash=False, repr=True)
+    """Whether this role is hoisting the members it's attached to in the member list.
+
+    members will be hoisted under their highest role where this is set to `builtins.True`.
+    """
+
+    is_mentionable: bool = attr.ib(eq=False, hash=False, repr=False)
+    """Whether this role can be mentioned by all regardless of permissions."""
+
+
+@attr_extensions.with_copy
+@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
+class TemplateGuild(guilds.PartialGuild):
+    """The partial guild object attached to `Template`."""
+
+    description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
+    """The guild's description, if set."""
+
+    region: str = attr.ib(eq=False, hash=False, repr=False)
+    """The voice region for the guild."""
+
+    verification_level: typing.Union[guilds.GuildVerificationLevel, int] = attr.ib(eq=False, hash=False, repr=False)
+    """The verification level needed for a user to participate in this guild."""
+
+    default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = attr.ib(
+        eq=False, hash=False, repr=False
+    )
+    """The default setting for message notifications in this guild."""
+
+    explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = attr.ib(
+        eq=False, hash=False, repr=False
+    )
+    """The setting for the explicit content filter in this guild."""
+
+    preferred_locale: str = attr.ib(eq=False, hash=False, repr=False)
+    """The preferred locale to use for this guild.
+
+    This can only be change if `GuildFeature.COMMUNITY` is in `Guild.features`
+    for this guild and will otherwise default to `en-US`.
+    """
+
+    afk_timeout: datetime.timedelta = attr.ib(eq=False, hash=False, repr=False)
+    """Timeout for activity before a member is classed as AFK.
+
+    How long a voice user has to be AFK for before they are classed as being
+    AFK and are moved to the AFK channel (`Guild.afk_channel_id`).
+    """
+
+    roles: typing.Mapping[snowflakes.Snowflake, TemplateRole] = attr.ib(eq=False, hash=False, repr=False)
+    """The roles in the guild.
+
+    !!! note
+        `hikari.guilds.Role.id` will be a unique placeholder on all the role
+        objects found attached this template guild.
+    """
+
+    channels: typing.Mapping[snowflakes.Snowflake, channels_.GuildChannel] = attr.ib(eq=False, hash=False, repr=False)
+    """The channels for the guild.
+
+    !!! note
+        `hikari.channels.GuildChannel.id` will be a unique placeholder on all
+        the channel objects found attached this template guild.
+    """
+
+    afk_channel_id: typing.Optional[snowflakes.Snowflake] = attr.ib(eq=False, hash=False, repr=False)
+    """The ID for the channel that AFK voice users get sent to.
+
+    If `builtins.None`, then no AFK channel is set up for this guild.
+    """
+
+    system_channel_id: typing.Optional[snowflakes.Snowflake] = attr.ib(eq=False, hash=False, repr=False)
+    """The ID of the system channel or `builtins.None` if it is not enabled.
+
+    Welcome messages and Nitro boost messages may be sent to this channel.
+    """
+
+    system_channel_flags: guilds.GuildSystemChannelFlag = attr.ib(eq=False, hash=False, repr=False)
+    """Return flags for the guild system channel.
+
+    These are used to describe which notifications are suppressed.
+
+    Returns
+    -------
+    GuildSystemChannelFlag
+        The system channel flags for this channel.
+    """
+
+
+@attr_extensions.with_copy
+@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
+class Template:
+    """Represents a template used for creating guilds."""
+
+    code: str = attr.ib(eq=True, hash=True, repr=True)
+    """The template's unique ID."""
+
+    name: str = attr.ib(eq=False, hash=False, repr=True)
+    """The template's name."""
+
+    description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False)
+    """The template's description."""
+
+    usage_count: int = attr.ib(eq=False, hash=False, repr=True)
+    """The number of times the template has been used to create a guild."""
+
+    creator: users.User = attr.ib(eq=False, hash=False, repr=False)
+    """The user who created the template."""
+
+    created_at: datetime.datetime = attr.ib(eq=False, hash=False, repr=True)
+    """When the template was created."""
+
+    updated_at: datetime.datetime = attr.ib(eq=False, hash=False, repr=True)
+    """When the template was last synced with the source guild."""
+
+    source_guild: TemplateGuild = attr.ib(eq=False, hash=False, repr=True)
+    """The partial object of the guild this template is based on."""
+
+    is_dirty: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False)
+    """Whether this template is missing changes from it's source guild."""
+
+    def __str__(self) -> str:
+        return f"https://discord.new/{self.code}"
+
+
+Templateish = typing.Union[str, Template]
+"""Type hint for a `Template` object or `builtin.str` template code."""


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index f9c14bef..6c25912d 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -603,6 +603,10 @@ def test_deserialize_dm_channel_with_null_fields(self, entity_factory_impl, user
         )
         assert dm_channel.last_message_id is None
 
+    def test_deserialize_dm_channel_with_unsetfields(self, entity_factory_impl, user_payload):
+        dm_channel = entity_factory_impl.deserialize_dm({"id": "123", "type": 1, "recipients": [user_payload]})
+        assert dm_channel.last_message_id is None
+
     @pytest.fixture()
     def group_dm_channel_payload(self, user_payload):
         return {
@@ -637,13 +641,13 @@ def test_test_deserialize_group_dm_channel_with_unset_fields(self, entity_factor
                 "name": "Secret Developer Group",
                 "icon": "123asdf123adsf",
                 "owner_id": "456",
-                "last_message_id": None,
                 "type": 3,
                 "recipients": [user_payload],
             }
         )
         assert group_dm.nicknames == {}
         assert group_dm.application_id is None
+        assert group_dm.last_message_id is None
 
     @pytest.fixture()
     def guild_category_payload(self, permission_overwrite_payload):
@@ -753,7 +757,6 @@ def test_deserialize_guild_text_channel_with_unset_fields(self, entity_factory_i
                 "position": 6,
                 "permission_overwrites": [],
                 "topic": "¯\\_(ツ)_/¯",
-                "last_message_id": "123456",
                 "guild_id": "123123123",
             }
         )
@@ -761,6 +764,7 @@ def test_deserialize_guild_text_channel_with_unset_fields(self, entity_factory_i
         assert guild_text_channel.rate_limit_per_user.total_seconds() == 0
         assert guild_text_channel.last_pin_timestamp is None
         assert guild_text_channel.parent_id is None
+        assert guild_text_channel.last_message_id is None
 
     def test_deserialize_guild_text_channel_with_null_fields(self, entity_factory_impl):
         guild_text_channel = entity_factory_impl.deserialize_guild_text_channel(
@@ -831,13 +835,13 @@ def test_deserialize_guild_news_channel_with_unset_fields(self, entity_factory_i
                 "position": 0,
                 "permission_overwrites": [],
                 "topic": "Super Important Announcements",
-                "last_message_id": "456",
                 "guild_id": "4123",
             }
         )
         assert news_channel.is_nsfw is None
         assert news_channel.parent_id is None
         assert news_channel.last_pin_timestamp is None
+        assert news_channel.last_message_id is None
 
     def test_deserialize_guild_news_channel_with_null_fields(self, entity_factory_impl):
         news_channel = entity_factory_impl.deserialize_guild_news_channel(
@@ -1887,14 +1891,14 @@ def test_deserialize_rest_guild_with_unset_fields(self, entity_factory_impl):
                 "verification_level": 4,
                 "max_presences": 8,
                 "max_members": 9,
-                "approximate_member_count": 42,
-                "approximate_presence_count": 9,
             }
         )
         assert guild.max_video_channel_users is None
         assert guild.premium_subscription_count is None
         assert guild.widget_channel_id is None
         assert guild.is_widget_enabled is None
+        assert guild.approximate_active_member_count is None
+        assert guild.approximate_member_count is None
 
     def test_deserialize_rest_guild_with_null_fields(self, entity_factory_impl):
         guild = entity_factory_impl.deserialize_rest_guild(
@@ -2952,6 +2956,143 @@ def test_deserialize_member_presence_with_unset_activity_sub_fields(self, entity
         assert activity.secrets.spectate is None
         assert activity.secrets.match is None
 
+    ###################
+    # TEMPLATE MODELS #
+    ###################
+
+    @pytest.fixture()
+    def template_payload(self, guild_text_channel_payload, user_payload):
+        return {
+            "code": "4rDaewUKeYVj",
+            "name": "ttt",
+            "description": "eee",
+            "usage_count": 42,
+            "creator_id": "115590097100865541",
+            "creator": user_payload,
+            "created_at": "2020-12-15T01:54:35+00:00",
+            "updated_at": "2020-12-15T01:57:35+00:00",
+            "source_guild_id": "574921006817476608",
+            "serialized_source_guild": {
+                "name": "hikari",
+                "description": "a descript description",
+                "region": "europe",
+                "icon_hash": "27b75989b5b42aba51346a6b69d8fcfe",
+                "verification_level": 2,
+                "default_message_notifications": 1,
+                "explicit_content_filter": 2,
+                "preferred_locale": "en-GB",
+                "afk_timeout": 3600,
+                "roles": [
+                    {
+                        "id": "33",
+                        "name": "@everyone",
+                        "color": 0,
+                        "hoist": True,
+                        "mentionable": False,
+                        "permissions": "104189505",
+                    }
+                ],
+                "channels": [guild_text_channel_payload],
+                "afk_channel_id": "321123",
+                "system_channel_id": "8",
+                "system_channel_flags": 0,
+            },
+            "is_dirty": True,
+        }
+
+    def test_deserialize_template(
+        self, entity_factory_impl, mock_app, template_payload, user_payload, guild_text_channel_payload
+    ):
+        template = entity_factory_impl.deserialize_template(template_payload)
+        assert template.code == "4rDaewUKeYVj"
+        assert template.name == "ttt"
+        assert template.description == "eee"
+        assert template.usage_count == 42
+        assert template.creator == entity_factory_impl.deserialize_user(user_payload)
+        assert template.created_at == datetime.datetime(2020, 12, 15, 1, 54, 35, tzinfo=datetime.timezone.utc)
+        assert template.updated_at == datetime.datetime(2020, 12, 15, 1, 57, 35, tzinfo=datetime.timezone.utc)
+
+        # TemplateGuild
+        assert template.source_guild.app is mock_app
+        assert template.source_guild.id == 574921006817476608
+        assert template.source_guild.icon_hash == "27b75989b5b42aba51346a6b69d8fcfe"
+        assert template.source_guild.name == "hikari"
+        assert template.source_guild.description == "a descript description"
+        assert template.source_guild.region == "europe"
+        assert template.source_guild.verification_level is guild_models.GuildVerificationLevel.MEDIUM
+        assert (
+            template.source_guild.default_message_notifications
+            is guild_models.GuildMessageNotificationsLevel.ONLY_MENTIONS
+        )
+        assert template.source_guild.explicit_content_filter is guild_models.GuildExplicitContentFilterLevel.ALL_MEMBERS
+        assert template.source_guild.preferred_locale == "en-GB"
+        assert template.source_guild.afk_timeout == datetime.timedelta(seconds=3600)
+
+        # TemplateRole
+        assert len(template.source_guild.roles) == 1
+        role = template.source_guild.roles[33]
+        assert role.app is mock_app
+        assert role.id == 33
+        assert role.name == "@everyone"
+        assert role.permissions == permission_models.Permissions(104189505)
+        assert role.color == color_models.Color(0)
+        assert role.is_hoisted is True
+        assert role.is_mentionable is False
+
+        assert template.source_guild.channels == {
+            123: entity_factory_impl.deserialize_channel(guild_text_channel_payload)
+        }
+        assert template.source_guild.afk_channel_id == 321123
+        assert template.source_guild.system_channel_id == 8
+        assert template.source_guild.system_channel_flags == guild_models.GuildSystemChannelFlag.NONE
+
+        assert template.is_dirty is True
+
+    def test_deserialize_template_with_null_fields(self, entity_factory_impl, template_payload, user_payload):
+        template = entity_factory_impl.deserialize_template(
+            {
+                "code": "4rDaewUKeYVj",
+                "name": "ttt",
+                "description": None,
+                "usage_count": 42,
+                "creator_id": "115590097100865541",
+                "creator": user_payload,
+                "created_at": "2020-12-15T01:54:35+00:00",
+                "updated_at": "2020-12-15T01:57:35+00:00",
+                "source_guild_id": "574921006817476608",
+                "serialized_source_guild": {
+                    "name": "hikari",
+                    "description": "a descript description",
+                    "region": "europe",
+                    "icon_hash": "27b75989b5b42aba51346a6b69d8fcfe",
+                    "verification_level": 2,
+                    "default_message_notifications": 1,
+                    "explicit_content_filter": 2,
+                    "preferred_locale": "en-GB",
+                    "afk_timeout": 3600,
+                    "roles": [
+                        {
+                            "id": "33",
+                            "name": "@everyone",
+                            "color": 0,
+                            "hoist": True,
+                            "mentionable": False,
+                            "permissions": "104189505",
+                        }
+                    ],
+                    "channels": [],
+                    "afk_channel_id": None,
+                    "system_channel_id": None,
+                    "system_channel_flags": 0,
+                },
+                "is_dirty": None,
+            }
+        )
+        assert template.description is None
+        assert template.source_guild.afk_channel_id is None
+        assert template.source_guild.system_channel_id is None
+        assert template.is_dirty is None
+
     ###############
     # USER MODELS #
     ###############
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 9f092d05..f33a249b 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -2635,3 +2635,100 @@ async def test_fetch_vanity_url(self, rest_client):
         assert await rest_client.fetch_vanity_url(StubModel(123)) == vanity_url
         rest_client._request.assert_awaited_once_with(expected_route)
         rest_client._entity_factory.deserialize_vanity_url.assert_called_once_with({"id": "789"})
+
+    async def test_create_template_without_description(self, rest_client):
+        expected_routes = routes.POST_GUILD_TEMPLATES.compile(guild=1235432)
+        rest_client._request = mock.AsyncMock(return_value={"code": "94949sdfkds"})
+
+        result = await rest_client.create_template(StubModel(1235432), "OKOKOK")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_routes, json={"name": "OKOKOK"})
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "94949sdfkds"})
+
+    async def test_create_template_with_description(self, rest_client):
+        expected_route = routes.POST_GUILD_TEMPLATES.compile(guild=4123123)
+        rest_client._request = mock.AsyncMock(return_value={"code": "76345345"})
+
+        result = await rest_client.create_template(StubModel(4123123), "33", description="43123123")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_route, json={"name": "33", "description": "43123123"})
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "76345345"})
+
+    async def test_create_guild_from_template_without_icon(self, rest_client):
+        expected_route = routes.POST_TEMPLATE.compile(template="odkkdkdkd")
+        rest_client._request = mock.AsyncMock(return_value={"id": "543123123"})
+
+        result = await rest_client.create_guild_from_template("odkkdkdkd", "ok a name")
+        assert result is rest_client._entity_factory.deserialize_rest_guild.return_value
+        rest_client._request.assert_awaited_once_with(expected_route, json={"name": "ok a name"})
+        rest_client._entity_factory.deserialize_rest_guild.assert_called_once_with({"id": "543123123"})
+
+    async def test_create_guild_from_template_with_icon(self, rest_client, file_resource):
+        expected_route = routes.POST_TEMPLATE.compile(template="odkkdkdkd")
+        rest_client._request = mock.AsyncMock(return_value={"id": "543123123"})
+        icon_resource = file_resource("icon data")
+
+        with mock.patch.object(files, "ensure_resource", return_value=icon_resource):
+            result = await rest_client.create_guild_from_template("odkkdkdkd", "ok a name", icon="icon.png")
+
+        assert result is rest_client._entity_factory.deserialize_rest_guild.return_value
+        rest_client._request.assert_awaited_once_with(expected_route, json={"name": "ok a name", "icon": "icon data"})
+        rest_client._entity_factory.deserialize_rest_guild.assert_called_once_with({"id": "543123123"})
+
+    async def delete_template(self, rest_client):
+        expected_route = routes.DELETE_GUILD_TEMPLATE(guild=34123123, template="945949494394")
+        rest_client._request = mock.AsyncMock(return_value={"code": "oeoekfgkdkf"})
+
+        result = await rest_client.delete_template(StubModel(3123123), "eoiesri9er99")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "oeoekfgkdkf"})
+
+    async def test_edit_template_without_optionals(self, rest_client):
+        expected_route = routes.PATCH_GUILD_TEMPLATE.compile(guild=3412312, template="oeodsosda")
+        rest_client._request = mock.AsyncMock(return_value={"code": "9493293ikiwopop"})
+
+        result = await rest_client.edit_template(StubModel(3412312), "oeodsosda")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_route, json={})
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "9493293ikiwopop"})
+
+    async def test_edit_template_with_optionals(self, rest_client):
+        expected_route = routes.PATCH_GUILD_TEMPLATE.compile(guild=34123122, template="oeodsosda2")
+        rest_client._request = mock.AsyncMock(return_value={"code": "9493293ikiwopop"})
+
+        result = await rest_client.edit_template(
+            StubModel(34123122), "oeodsosda2", name="new name", description="i'm lazy"
+        )
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(
+            expected_route, json={"name": "new name", "description": "i'm lazy"}
+        )
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "9493293ikiwopop"})
+
+    async def test_fetch_template(self, rest_client):
+        expected_route = routes.GET_TEMPLATE.compile(template="kodfskoijsfikoiok")
+        rest_client._request = mock.AsyncMock(return_value={"code": "KSDAOKSDKIO"})
+
+        result = await rest_client.fetch_template("kodfskoijsfikoiok")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "KSDAOKSDKIO"})
+
+    async def test_fetch_guild_templates(self, rest_client):
+        expected_route = routes.GET_GUILD_TEMPLATES.compile(guild=43123123)
+        rest_client._request = mock.AsyncMock(return_value=[{"code": "jirefu98ai90w"}])
+
+        result = await rest_client.fetch_guild_templates(StubModel(43123123))
+        assert result == [rest_client._entity_factory.deserialize_template.return_value]
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "jirefu98ai90w"})
+
+    async def test_sync_guild_template(self, rest_client):
+        expected_route = routes.PUT_GUILD_TEMPLATE.compile(guild=431231, template="oeroeoeoeoeo")
+        rest_client._request = mock.AsyncMock(return_value={"code": "ldsaosdokskdoa"})
+
+        result = await rest_client.sync_guild_template(StubModel(431231), template="oeroeoeoeoeo")
+        assert result is rest_client._entity_factory.deserialize_template.return_value
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_template.assert_called_once_with({"code": "ldsaosdokskdoa"})
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index db30e13a..6ae0dd03 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -309,7 +309,6 @@ class TestPartialGuild:
     def model(self, mock_app):
         return guilds.PartialGuild(
             app=mock_app,
-            features=["foo", "bar", "baz"],
             id=snowflakes.Snowflake(90210),
             icon_hash="yeet",
             name="hikari",