#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © Nekokatt 2019-2020
#
# This file is part of Hikari.
#
# Hikari is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Hikari is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Hikari. If not, see <https://www.gnu.org/licenses/>.
"""Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API."""
__all__ = ["LowLevelRestfulClient"]

import asyncio
import contextlib
import datetime
import email.utils
import json
import logging
import ssl
import types
import typing
import uuid

import aiohttp.typedefs

from hikari import errors
from hikari.internal import assertions
from hikari.internal import conversions
from hikari.internal import more_collections
from hikari.internal import more_logging
from hikari.internal import urls
from hikari.net import codes
from hikari.net import ratelimits
from hikari.net import routes
from hikari.net import user_agent
from hikari.net import versions


class LowLevelRestfulClient:
    """A RESTful client to allow you to interact with the Discord API.

    Parameters
    ----------
    base_url: :obj:`str`
        The base URL and route for the discord API
    allow_redirects: :obj:`bool`
        Whether to allow redirects or not.
    connector: :obj:`aiohttp.BaseConnector`, optional
        Optional aiohttp connector info for making an HTTP connection
    proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional
        Optional proxy headers to pass to HTTP requests.
    proxy_auth: :obj:`aiohttp.BasicAuth`, optional
        Optional authorization to be used if using a proxy.
    proxy_url: :obj:`str`, optional
        Optional proxy URL to use for HTTP requests.
    ssl_context: :obj:`ssl.SSLContext`, optional
        The optional SSL context to be used.
    verify_ssl: :obj:`bool`
        Whether or not the client should enforce SSL signed certificate
        verification. If :obj:`False` it will ignore potentially malicious
        SSL certificates.
    timeout: :obj:`float`, optional
        The optional timeout for all HTTP requests.
    json_deserialize: ``deserialization function``
        A custom JSON deserializer function to use. Defaults to
        :func:`json.loads`.
    json_serialize: ``serialization function``
        A custom JSON serializer function to use. Defaults to
        :func:`json.dumps`.
    token: :obj:`string`, optional
        The bot token for the client to use. You may start this with
        a prefix of either ``Bot`` or ``Bearer`` to force the token type, or
        not provide this information if you want to have it auto-detected.
        If this is passed as :obj:`None`, then no token is used.
        This will be passed as the ``Authorization`` header if not :obj:`None`
        for each request.
    version: :obj:`typing.Union` [ :obj:`int`, :obj:`hikari.net.versions.HTTPAPIVersion` ]
        The version of the API to use. Defaults to the most recent stable
        version.
    """

    GET = "get"
    POST = "post"
    PATCH = "patch"
    PUT = "put"
    HEAD = "head"
    DELETE = "delete"
    OPTIONS = "options"

    _AUTHENTICATION_SCHEMES = ("Bearer", "Bot")

    #: :obj:`True` if HTTP redirects are enabled, or :obj:`False` otherwise.
    #:
    #: :type: :obj:`bool`
    allow_redirects: bool

    #: The base URL to send requests to.
    #:
    #: :type: :obj:`str`
    base_url: str

    #: The :mod:`aiohttp` client session to make requests with.
    #:
    #: :type: :obj:`aiohttp.ClientSession`
    client_session: aiohttp.ClientSession

    #: The internal correlation ID for the number of requests sent. This will
    #: increase each time a REST request is sent.
    #:
    #: :type: :obj:`int`
    in_count: int

    #: The global ratelimiter. This is used if Discord declares a ratelimit
    #: across the entire API, regardless of the endpoint. If this is set, then
    #: any HTTP operation using this session will be paused.
    #:
    #: :type: :obj:`hikari.net.ratelimits.ManualRateLimiter`
    global_ratelimiter: ratelimits.ManualRateLimiter

    #: The logger to use for this object.
    #:
    #: :type: :obj:`logging.Logger`
    logger: logging.Logger

    #: The JSON deserialization function. This consumes a JSON string and
    #: produces some object.
    json_deserialize: typing.Callable[[typing.AnyStr], typing.Any]

    #: The JSON deserialization function. This consumes an object and
    #: produces some JSON string.
    json_serialize: typing.Callable[[typing.Any], typing.AnyStr]

    #: Proxy authorization to use.
    #:
    #: :type: :obj:`aiohttp.BasicAuth`, optional
    proxy_auth: typing.Optional[aiohttp.BasicAuth]

    #: A set of headers to provide to a proxy server.
    #:
    #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional
    proxy_headers: typing.Optional[typing.Mapping[str, str]]

    #: An optional proxy URL to send requests to.
    #:
    #: :type: :obj:`str`, optional
    proxy_url: typing.Optional[str]

    #: The per-route ratelimit manager. This handles tracking any ratelimits
    #: for routes that have recently been used or are in active use, as well
    #: as keeping memory usage to a minimum where possible for large numbers
    #: of varying requests. This encapsulates a lot of complex rate limiting
    #: rules to reduce the number of active ``429`` responses this client gets,
    #: and thus reducing your chances of an API ban by Discord.
    #:
    #: You should not ever need to touch this implementation.
    #:
    #: :type: :obj:`hikari.net.ratelimits.HTTPBucketRateLimiterManager`
    ratelimiter: ratelimits.HTTPBucketRateLimiterManager

    #: The custom SSL context to use.
    #:
    #: :type: :obj:`ssl.SSLContext`
    ssl_context: typing.Optional[ssl.SSLContext]

    #: The HTTP request timeout to abort requests after.
    #:
    #: :type: :obj:`float`
    timeout: typing.Optional[float]

    #: The bot token. This will be prefixed with either ``"Bearer "`` or
    #: ``"Bot"`` depending on the format of the token passed to the constructor.
    #:
    #: This value will be used for the ``Authorization`` HTTP header on each
    #: API request.
    #:
    #: If no token is set, then the value will be :obj:`None`. In this case,
    #: no ``Authorization`` header will be sent.
    #:
    #: :type: :obj:`str`, optional
    token: typing.Optional[str]

    #: The ``User-Agent`` header to send to Discord.
    #:
    #: Warning
    #: -------
    #: Changing this value may lead to undesirable results, as Discord document
    #: that they can actively IP ban any client that does not have a valid
    #: ``User-Agent`` header that conforms to specific requirements.
    #: Your mileage may vary (YMMV).
    #:
    #: :type: :obj:`str`
    user_agent: str

    #: If :obj:`True`, SSL certificates are verified for each request, and
    #: invalid SSL certificates are rejected, causing an exception. If
    #: :obj:`False`, then unrecognised certificates that may be illegitimate
    #: are accepted and ignored.
    #:
    #: :type: :obj:`bool`
    verify_ssl: bool

    #: The API version number that is being used.
    #:
    #: :type: :obj:`int`
    version: int

    def __init__(
        self,
        *,
        base_url: str = urls.REST_API_URL,
        allow_redirects: bool = False,
        connector: typing.Optional[aiohttp.BaseConnector] = None,
        proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None,
        proxy_auth: typing.Optional[aiohttp.BasicAuth] = None,
        proxy_url: typing.Optional[str] = None,
        ssl_context: typing.Optional[ssl.SSLContext] = None,
        verify_ssl: bool = True,
        timeout: typing.Optional[float] = None,
        json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads,
        json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps,
        token: typing.Optional[str],
        version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE,
    ) -> None:
        self.allow_redirects = allow_redirects
        self.client_session = aiohttp.ClientSession(
            connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps,
        )
        self.logger = more_logging.get_named_logger(self)
        self.user_agent = user_agent.UserAgent().user_agent
        self.verify_ssl = verify_ssl
        self.proxy_url = proxy_url
        self.proxy_auth = proxy_auth
        self.proxy_headers = proxy_headers
        self.ssl_context: ssl.SSLContext = ssl_context
        self.timeout = timeout
        self.in_count = 0
        self.version = int(version)
        self.base_url = base_url.format(self)
        self.global_ratelimiter = ratelimits.ManualRateLimiter()
        self.json_serialize = json_serialize
        self.json_deserialize = json_deserialize
        self.ratelimiter = ratelimits.HTTPBucketRateLimiterManager()
        self.ratelimiter.start()

        if token is not None and not token.startswith(self._AUTHENTICATION_SCHEMES):
            this_type = type(self).__name__
            auth_schemes = " or ".join(self._AUTHENTICATION_SCHEMES)
            raise RuntimeError(f"Any token passed to {this_type} should begin with {auth_schemes}")

        self.token = token

    async def close(self) -> None:
        """Shut down the REST client safely, and terminate any rate limiters executing in the background."""
        with contextlib.suppress(Exception):
            self.ratelimiter.close()
        with contextlib.suppress(Exception):
            self.logger.debug("Closing %s", type(self).__qualname__)
            await self.client_session.close()

    async def __aenter__(self) -> "LowLevelRestfulClient":
        return self

    async def __aexit__(
        self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType
    ) -> None:
        await self.close()

    async def _request(
        self,
        compiled_route: routes.CompiledRoute,
        *,
        headers: typing.Optional[typing.Dict[str, str]] = None,
        query: typing.Optional[typing.Dict[str, typing.Any]] = None,
        form_body: typing.Optional[aiohttp.FormData] = None,
        json_body: typing.Optional[typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any]]] = None,
        reason: str = ...,
        re_seekable_resources: typing.Collection[conversions.Seekable] = more_collections.EMPTY_COLLECTION,
        suppress_authorization_header: bool = False,
        **kwargs,
    ) -> typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any], None]:
        bucket_ratelimit_future = self.ratelimiter.acquire(compiled_route)
        request_headers = {"X-RateLimit-Precision": "millisecond"}

        if self.token is not None and not suppress_authorization_header:
            request_headers["Authorization"] = self.token

        if reason and reason is not ...:
            request_headers["X-Audit-Log-Reason"] = reason

        if headers is not None:
            request_headers.update(headers)

        backoff = ratelimits.ExponentialBackOff()

        seeks = {r.tell(): r for r in re_seekable_resources}

        while True:
            # If we are uploading files with io objects in a form body, we need to reset the seeks to 0 to ensure
            # we can re-read the buffer.
            for pos, r in seeks.items():
                r.seek(pos)

            # Aids logging when lots of entries are being logged at once by matching a unique UUID
            # between the request and response
            request_uuid = uuid.uuid4()

            await asyncio.gather(bucket_ratelimit_future, self.global_ratelimiter.acquire())

            if json_body is not None:
                body_type = "json"
            elif form_body is not None:
                body_type = "form"
            else:
                body_type = "None"

            self.logger.debug(
                "%s send to %s headers=%s query=%s body_type=%s body=%s",
                request_uuid,
                compiled_route,
                request_headers,
                query,
                body_type,
                json_body if json_body is not None else form_body,
            )

            async with self.client_session.request(
                compiled_route.method,
                compiled_route.create_url(self.base_url),
                headers=request_headers,
                json=json_body,
                params=query,
                data=form_body,
                allow_redirects=self.allow_redirects,
                proxy=self.proxy_url,
                proxy_auth=self.proxy_auth,
                proxy_headers=self.proxy_headers,
                verify_ssl=self.verify_ssl,
                ssl_context=self.ssl_context,
                timeout=self.timeout,
                **kwargs,
            ) as resp:
                raw_body = await resp.read()
                headers = resp.headers

                self.logger.debug(
                    "%s recv from %s status=%s reason=%s headers=%s body=%s",
                    request_uuid,
                    compiled_route,
                    resp.status,
                    resp.reason,
                    headers,
                    raw_body,
                )

                limit = int(headers.get("X-RateLimit-Limit", "1"))
                remaining = int(headers.get("X-RateLimit-Remaining", "1"))
                bucket = headers.get("X-RateLimit-Bucket", "None")
                reset = float(headers.get("X-RateLimit-Reset", "0"))
                reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc)
                now_date = email.utils.parsedate_to_datetime(headers["Date"])
                content_type = headers.get("Content-Type")

                status = resp.status

                with contextlib.suppress(ValueError):
                    status = codes.HTTPStatusCode(status)

                if status == codes.HTTPStatusCode.NO_CONTENT:
                    body = None
                elif content_type == "application/json":
                    body = self.json_deserialize(raw_body)
                elif content_type in ("text/plain", "text/html"):
                    await self._handle_bad_response(
                        backoff,
                        status,
                        compiled_route,
                        f"Received unexpected response of type {content_type} with body: {raw_body!r}",
                        None,
                    )
                    continue
                else:
                    body = None

            self.ratelimiter.update_rate_limits(compiled_route, bucket, remaining, limit, now_date, reset_date)

            if status == codes.HTTPStatusCode.TOO_MANY_REQUESTS:
                # We are being rate limited.
                if body["global"]:
                    retry_after = float(body["retry_after"]) / 1_000
                    self.global_ratelimiter.throttle(retry_after)
                continue

            if status >= codes.HTTPStatusCode.BAD_REQUEST:
                code = None

                if self.version == versions.HTTPAPIVersion.V6:
                    message = ", ".join(f"{k} - {v}" for k, v in body.items())
                else:
                    message = body.get("message")
                    with contextlib.suppress(ValueError):
                        code = codes.JSONErrorCode(body.get("code"))

                if status == codes.HTTPStatusCode.BAD_REQUEST:
                    raise errors.BadRequestHTTPError(compiled_route, message, code)
                if status == codes.HTTPStatusCode.UNAUTHORIZED:
                    raise errors.UnauthorizedHTTPError(compiled_route, message, code)
                if status == codes.HTTPStatusCode.FORBIDDEN:
                    raise errors.ForbiddenHTTPError(compiled_route, message, code)
                if status == codes.HTTPStatusCode.NOT_FOUND:
                    raise errors.NotFoundHTTPError(compiled_route, message, code)
                if status < codes.HTTPStatusCode.INTERNAL_SERVER_ERROR:
                    raise errors.ClientHTTPError(status, compiled_route, message, code)

                await self._handle_bad_response(backoff, status, compiled_route, message, code)
                continue

            return body

    async def _handle_bad_response(
        self,
        backoff: ratelimits.ExponentialBackOff,
        status: typing.Union[codes.HTTPStatusCode, int, None],
        route: routes.CompiledRoute,
        message: typing.Optional[str],
        code: typing.Union[codes.JSONErrorCode, int, None],
    ) -> None:
        try:
            next_sleep = next(backoff)
            self.logger.warning("received a server error response, backing off for %ss and trying again", next_sleep)
            await asyncio.sleep(next_sleep)
        except asyncio.TimeoutError:
            raise errors.ServerHTTPError(status, route, message, code)

    async def get_gateway(self) -> str:
        """Get the URL to use to connect to the gateway with.

        Returns
        -------
        :obj:`str`
            A static URL to use to connect to the gateway with.

        Note
        ----
        Users are expected to attempt to cache this result.
        """
        result = await self._request(routes.GATEWAY.compile(self.GET))
        return result["url"]

    async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]:
        """Get the gateway info for the bot.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            An object containing a ``url`` to connect to, an :obj:`int` number of shards recommended to use
            for connecting, and a ``session_start_limit`` object.

        Note
        ----
        Unlike :meth:`get_gateway`, this requires a valid token to work.
        """
        return await self._request(routes.GATEWAY_BOT.compile(self.GET))

    async def get_guild_audit_log(
        self, guild_id: str, *, user_id: str = ..., action_type: int = ..., limit: int = ..., before: str = ...
    ) -> typing.Dict:
        """Get an audit log object for the given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The guild ID to look up.
        user_id : :obj:`str`
            If specified, the user ID to filter by.
        action_type : :obj:`int`
            If specified, the action type to look up.
        limit : :obj:`int`
            If specified, the limit to apply to the number of records.
            Defaults to ``50``. Must be between ``1`` and ``100`` inclusive.
        before : :obj:`str`
            If specified, the ID of the entry that all retrieved entries will
            have occurred before.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            An audit log object.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the given permissions to view an audit log.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild does not exist.
        """
        query = {}
        conversions.put_if_specified(query, "user_id", user_id)
        conversions.put_if_specified(query, "action_type", action_type)
        conversions.put_if_specified(query, "limit", limit)
        conversions.put_if_specified(query, "before", before)
        route = routes.GUILD_AUDIT_LOGS.compile(self.GET, guild_id=guild_id)
        return await self._request(route, query=query)

    async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]:
        """Get a channel object from a given channel ID.

        Parameters
        ----------
        channel_id : :obj:`str`
            The channel ID to look up.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The channel object that has been found.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you don't have access to the channel.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel does not exist.
        """
        route = routes.CHANNEL.compile(self.GET, channel_id=channel_id)
        return await self._request(route)

    async def modify_channel(  # lgtm [py/similar-function]
        self,
        channel_id: str,
        *,
        name: str = ...,
        position: int = ...,
        topic: str = ...,
        nsfw: bool = ...,
        rate_limit_per_user: int = ...,
        bitrate: int = ...,
        user_limit: int = ...,
        permission_overwrites: typing.Sequence[typing.Dict[str, typing.Any]] = ...,
        parent_id: str = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Update one or more aspects of a given channel ID.

        Parameters
        ----------
        channel_id : :obj:`str`
            The channel ID to update.
        name : :obj:`str`
            If specified, the new name for the channel. This must be
            between ``2`` and ``100`` characters in length.
        position : :obj:`int`
            If specified, the position to change the channel to.
        topic : :obj:`str`
            If specified, the topic to set. This is only applicable to
            text channels. This must be between ``0`` and ``1024``
            characters in length.
        nsfw : :obj:`bool`
            If specified, whether the  channel will be marked as NSFW.
            Only applicable to text channels.
        rate_limit_per_user : :obj:`int`
            If specified, the number of seconds the user has to wait before sending
            another message.  This will not apply to bots, or to members with
            ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must
            be between ``0`` and ``21600`` seconds.
        bitrate : :obj:`int`
            If specified, the bitrate in bits per second allowable for the channel.
            This only applies to voice channels and must be between ``8000``
            and ``96000`` for normal servers or ``8000`` and ``128000`` for
            VIP servers.
        user_limit : :obj:`int`
            If specified, the new max number of users to allow in a voice channel.
            This must be between ``0`` and ``99`` inclusive, where
            ``0`` implies no limit.
        permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            If specified, the new list of permission overwrites that are category
            specific to replace the existing overwrites with.
        parent_id : :obj:`str`, optional
            If specified, the new parent category ID to set for the channel.,
            pass :obj:`None` to unset.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The channel object that has been modified.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel does not exist.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the permission to make the change.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide incorrect options for the corresponding channel type
            (e.g. a ``bitrate`` for a text channel).
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "position", position)
        conversions.put_if_specified(payload, "topic", topic)
        conversions.put_if_specified(payload, "nsfw", nsfw)
        conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user)
        conversions.put_if_specified(payload, "bitrate", bitrate)
        conversions.put_if_specified(payload, "user_limit", user_limit)
        conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites)
        conversions.put_if_specified(payload, "parent_id", parent_id)
        route = routes.CHANNEL.compile(self.PATCH, channel_id=channel_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def delete_close_channel(self, channel_id: str) -> None:
        """Delete the given channel ID, or if it is a DM, close it.

        Parameters
        ----------
        channel_id : :obj:`str`
            The channel ID to delete, or direct message channel to close.

        Returns
        -------
        :obj:`None`
            Nothing, unlike what the API specifies. This is done to maintain
            consistency with other calls of a similar nature in this API wrapper.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel does not exist.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you do not have permission to delete the channel.

        Warning
        -------
        Deleted channels cannot be un-deleted. Deletion of DMs is able to be undone by reopening the DM.
        """
        route = routes.CHANNEL.compile(self.DELETE, channel_id=channel_id)
        await self._request(route)

    async def get_channel_messages(
        self, channel_id: str, *, limit: int = ..., after: str = ..., before: str = ..., around: str = ...,
    ) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Retrieve message history for a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to retrieve the messages from.
        limit : :obj:`int`
            If specified, the number of messages to return. Must be
            between ``1`` and ``100`` inclusive.Defaults to ``50``
            if unspecified.
        after : :obj:`str`
            A message ID. If specified, only return messages sent AFTER this message.
        before : :obj:`str`
            A message ID. If specified, only return messages sent BEFORE this message.
        around : :obj:`str`
            A message ID. If specified, only return messages sent AROUND and
            including (if it still exists) this message.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of message objects.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permission to read the channel.
        :obj:`hikari.errors.BadRequestHTTPError`
            If your query is malformed, has an invalid value for ``limit``,
            or contains more than one of ``after``, ``before`` and ``around``.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found, or the message
            provided for one of the filter arguments is not found.

        Note
        ----
        If you are missing the ``VIEW_CHANNEL`` permission, you will receive a
        :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing
        the ``READ_MESSAGE_HISTORY`` permission, you will always receive
        zero results, and thus an empty list will be returned instead.

        Warning
        -------
        You can only specify a maximum of one from ``before``, ``after``, and ``around``.
        Specifying more than one will cause a :obj:`hikari.errors.BadRequestHTTPError` to be raised.
        """
        query = {}
        conversions.put_if_specified(query, "limit", limit)
        conversions.put_if_specified(query, "before", before)
        conversions.put_if_specified(query, "after", after)
        conversions.put_if_specified(query, "around", around)
        route = routes.CHANNEL_MESSAGES.compile(self.GET, channel_id=channel_id)
        return await self._request(route, query=query)

    async def get_channel_message(self, channel_id: str, message_id: str) -> typing.Dict[str, typing.Any]:
        """Get the message with the given message ID from the channel with the given channel ID.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to retrieve.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            A message object.

        Note
        ----
        This requires the ``READ_MESSAGE_HISTORY`` permission.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permission to see the message.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found.
        """
        route = routes.CHANNEL_MESSAGE.compile(self.GET, channel_id=channel_id, message_id=message_id)
        return await self._request(route)

    async def create_message(
        self,
        channel_id: str,
        *,
        content: str = ...,
        nonce: str = ...,
        tts: bool = ...,
        files: typing.Sequence[typing.Tuple[str, conversions.FileLikeT]] = ...,
        embed: typing.Dict[str, typing.Any] = ...,
        allowed_mentions: typing.Dict[str, typing.Any] = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a message in the given channel or DM.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to send to.
        content : :obj:`str`
            If specified, the message content to send with the message.
        nonce : :obj:`str`
            If specified, an optional ID to send for opportunistic message
            creation. This doesn't serve any real purpose for general use,
            and can usually be ignored.
        tts : :obj:`bool`
            If specified, whether the message will be sent as a TTS message.
        files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ] ]
            If specified, this should be a list of between ``1`` and ``5`` tuples.
            Each tuple should consist of the file name, and either
            raw :obj:`bytes` or an :obj:`io.IOBase` derived object with
            a seek that points to a buffer containing said file.
        embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            If specified, the embed to send with the message.
        allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            If specified, the mentions to parse from the ``content``.
            If not specified, will parse all mentions from the ``content``.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The created message object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.BadRequestHTTPError`
            This can be raised if the file is too large; if the embed exceeds
            the defined limits; if the message content is specified only and
            empty or greater than ``2000`` characters; if neither content, file
            or embed are specified; if there is a duplicate id in only of the
            fields in ``allowed_mentions``; if you specify to parse all
            users/roles mentions but also specify which users/roles to
            parse only.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permissions to send to this channel.
        """
        form = aiohttp.FormData()

        json_payload = {}
        conversions.put_if_specified(json_payload, "content", content)
        conversions.put_if_specified(json_payload, "nonce", nonce)
        conversions.put_if_specified(json_payload, "tts", tts)
        conversions.put_if_specified(json_payload, "embed", embed)
        conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions)

        form.add_field("payload_json", json.dumps(json_payload), content_type="application/json")

        re_seekable_resources = []
        if files is not ...:
            for i, (file_name, file) in enumerate(files):
                file = conversions.make_resource_seekable(file)
                re_seekable_resources.append(file)
                form.add_field(f"file{i}", file, filename=file_name, content_type="application/octet-stream")

        route = routes.CHANNEL_MESSAGES.compile(self.POST, channel_id=channel_id)
        return await self._request(route, form_body=form, re_seekable_resources=re_seekable_resources)

    async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None:
        """Add a reaction to the given message in the given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to add this reaction in.
        message_id : :obj:`str`
            The ID of the message to add the reaction in.
        emoji : :obj:`str`
            The emoji to add. This can either be a series of unicode
            characters making up a valid Discord emoji, or it can be a the url
            representation of a custom emoji ``<{emoji.name}:{emoji.id}>``.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If this is the first reaction using this specific emoji on this
            message and you lack the ``ADD_REACTIONS`` permission. If you lack
            ``READ_MESSAGE_HISTORY``, this may also raise this error.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found, or if the emoji is not found.
        :obj:`hikari.errors.BadRequestHTTPError`
            If the emoji is not valid, unknown, or formatted incorrectly.
        """
        route = routes.OWN_REACTION.compile(self.PUT, channel_id=channel_id, message_id=message_id, emoji=emoji)
        await self._request(route)

    async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str) -> None:
        """Remove your own reaction from the given message in the given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to delete the reaction from.
        emoji : :obj:`str`
            The emoji to delete. This can either be a series of unicode
            characters making up a valid Discord emoji, or it can be a
            snowflake ID for a custom emoji.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permission to do this.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message or emoji is not found.
        """
        route = routes.OWN_REACTION.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji)
        await self._request(route)

    async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, emoji: str) -> None:
        """Remove all reactions for a single given emoji on a given message in a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to delete the reactions from.
        emoji : :obj:`str`
            The emoji to delete. This can either be a series of unicode
            characters making up a valid Discord emoji, or it can be a
            snowflake ID for a custom emoji.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message or emoji or user is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs.
        """
        route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji)
        await self._request(route)

    async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: str) -> None:
        """Remove a reaction made by a given user using a given emoji on a given message in a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to remove the reaction from.
        emoji : :obj:`str`
            The emoji to delete. This can either be a series of unicode
            characters making up a valid Discord emoji, or it can be a
            snowflake ID for a custom emoji.
        user_id : :obj:`str`
            The ID of the user who made the reaction that you wish to remove.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message or emoji or user is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs.
        """
        route = routes.REACTION_EMOJI_USER.compile(
            self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji, user_id=user_id,
        )
        await self._request(route)

    async def get_reactions(
        self, channel_id: str, message_id: str, emoji: str, *, after: str = ..., limit: int = ...,
    ) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get a list of users who reacted with the given emoji on the given message.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to get the reactions from.
        emoji : :obj:`str`
            The emoji to get. This can either be a series of unicode
            characters making up a valid Discord emoji, or it can be a
            snowflake ID for a custom emoji.
        after : :obj:`str`
            If specified, the user ID. If specified, only users with a snowflake
            that is lexicographically greater than the value will be returned.
        limit : :obj:`str`
            If specified, the limit of the number of values to return. Must be
            between ``1`` and ``100`` inclusive. If unspecified,
            defaults to ``25``.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of user objects.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack access to the message.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found.
        """
        query = {}
        conversions.put_if_specified(query, "after", after)
        conversions.put_if_specified(query, "limit", limit)
        route = routes.REACTIONS.compile(self.GET, channel_id=channel_id, message_id=message_id, emoji=emoji)
        return await self._request(route, query=query)

    async def delete_all_reactions(self, channel_id: str, message_id: str) -> None:
        """Delete all reactions from a given message in a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to remove all reactions from.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission.
        """
        route = routes.ALL_REACTIONS.compile(self.DELETE, channel_id=channel_id, message_id=message_id)
        await self._request(route)

    async def edit_message(
        self,
        channel_id: str,
        message_id: str,
        *,
        content: typing.Optional[str] = ...,
        embed: typing.Optional[typing.Dict[str, typing.Any]] = ...,
        flags: int = ...,
        allowed_mentions: typing.Dict[str, typing.Any] = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Update the given message.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to edit.
        content : :obj:`str`, optional
            If specified, the string content to replace with in the message.
            If :obj:`None`, the content will be removed from the message.
        embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional
            If specified, the embed to replace with in the message.
            If :obj:`None`, the embed will be removed from the message.
        flags : :obj:`int`
            If specified, the integer to replace the message's current flags.
        allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            If specified, the mentions to parse from the ``content``.
            If not specified, will parse all mentions from the ``content``.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The edited message object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found.
        :obj:`hikari.errors.BadRequestHTTPError`
            This can be raised if the embed exceeds the defined limits;
            if the message content is specified only and empty or greater
            than ``2000`` characters; if neither content, file or embed
            are specified.
            parse only.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you try to edit ``content`` or ``embed`` or ``allowed_mentions`
            on a message you did not author or try to edit the flags on a
            message you did not author without the ``MANAGE_MESSAGES``
            permission.
        """
        payload = {}
        conversions.put_if_specified(payload, "content", content)
        conversions.put_if_specified(payload, "embed", embed)
        conversions.put_if_specified(payload, "flags", flags)
        conversions.put_if_specified(payload, "allowed_mentions", allowed_mentions)
        route = routes.CHANNEL_MESSAGE.compile(self.PATCH, channel_id=channel_id, message_id=message_id)
        return await self._request(route, json_body=payload)

    async def delete_message(self, channel_id: str, message_id: str) -> None:
        """Delete a message in a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        message_id : :obj:`str`
            The ID of the message to delete.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you did not author the message and are in a DM, or if you did not author the message and lack the
            ``MANAGE_MESSAGES`` permission in a guild channel.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel or message is not found.
        """
        route = routes.CHANNEL_MESSAGE.compile(self.DELETE, channel_id=channel_id, message_id=message_id)
        await self._request(route)

    async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[str]) -> None:
        """Delete multiple messages in a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the message from.
        messages : :obj:`typing.Sequence` [ :obj:`str` ]
            A list of ``2-100`` message IDs to remove in the channel.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission in the channel.
        :obj:`hikari.errors.BadRequestHTTPError`
            If any of the messages passed are older than ``2`` weeks in age or
            any duplicate message IDs are passed.

        Note
        ----
        This can only be used on guild text channels.
        Any message IDs that do not exist or are invalid still add towards the
        total ``100`` max messages to remove. This can only delete messages that
        are newer than ``2`` weeks in age. If any of the messages are older than
        ``2`` weeks then this call will fail.
        """
        payload = {"messages": messages}
        route = routes.CHANNEL_MESSAGES_BULK_DELETE.compile(self.POST, channel_id=channel_id)
        await self._request(route, json_body=payload)

    async def edit_channel_permissions(
        self, channel_id: str, overwrite_id: str, type_: str, *, allow: int = ..., deny: int = ..., reason: str = ...,
    ) -> None:
        """Edit permissions for a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to edit permissions for.
        overwrite_id : :obj:`str`
            The overwrite ID to edit.
        type_ : :obj:`str`
            The type of overwrite. ``"member"`` if it is for a member,
            or ``"role"`` if it is for a role.
        allow : :obj:`int`
            If specified, the bitwise value of all permissions to set to be allowed.
        deny : :obj:`int`
            If specified, the bitwise value of all permissions to set to be denied.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the target channel or overwrite doesn't exist.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permission to do this.
        """
        payload = {"type": type_}
        conversions.put_if_specified(payload, "allow", allow)
        conversions.put_if_specified(payload, "deny", deny)
        route = routes.CHANNEL_PERMISSIONS.compile(self.PATCH, channel_id=channel_id, overwrite_id=overwrite_id)
        await self._request(route, json_body=payload, reason=reason)

    async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get invites for a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get invites for.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of invite objects.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_CHANNELS`` permission.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel does not exist.
        """
        route = routes.CHANNEL_INVITES.compile(self.GET, channel_id=channel_id)
        return await self._request(route)

    async def create_channel_invite(
        self,
        channel_id: str,
        *,
        max_age: int = ...,
        max_uses: int = ...,
        temporary: bool = ...,
        unique: bool = ...,
        target_user: str = ...,
        target_user_type: int = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a new invite for the given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to create the invite for.
        max_age : :obj:`int`
            If specified, the max age of the invite in seconds, defaults to
            ``86400`` (``24`` hours).
            Set to ``0`` to never expire.
        max_uses : :obj:`int`
            If specified, the max number of uses this invite can have, or ``0``
            for unlimited (as per the default).
        temporary : :obj:`bool`
            If specified, whether to grant temporary membership, meaning the
            user is kicked when their session ends unless they are given a role.
        unique : :obj:`bool`
            If specified, whether to try to reuse a similar invite.
        target_user : :obj:`str`
            If specified, the ID of the user this invite should target.
        target_user_type : :obj:`int`
            If specified, the type of target for this invite.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            An invite object.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``CREATE_INSTANT_MESSAGES`` permission.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel does not exist.
        :obj:`hikari.errors.BadRequestHTTPError`
            If the arguments provided are not valid (e.g. negative age, etc).
        """
        payload = {}
        conversions.put_if_specified(payload, "max_age", max_age)
        conversions.put_if_specified(payload, "max_uses", max_uses)
        conversions.put_if_specified(payload, "temporary", temporary)
        conversions.put_if_specified(payload, "unique", unique)
        conversions.put_if_specified(payload, "target_user", target_user)
        conversions.put_if_specified(payload, "target_user_type", target_user_type)
        route = routes.CHANNEL_INVITES.compile(self.POST, channel_id=channel_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> None:
        """Delete a channel permission overwrite for a user or a role in a channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to delete the overwrite from.
        overwrite_id : :obj:`str`
            The ID of the overwrite to remove.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the overwrite or channel do not exist.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission for that channel.
        """
        route = routes.CHANNEL_PERMISSIONS.compile(self.DELETE, channel_id=channel_id, overwrite_id=overwrite_id)
        await self._request(route)

    async def trigger_typing_indicator(self, channel_id: str) -> None:
        """Trigger the account to appear to be typing for the next ``10`` seconds in the given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to appear to be typing in.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not able to type in the channel.
        """
        route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id)
        await self._request(route)

    async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get pinned messages for a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The channel ID to get messages from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of messages.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not able to see the channel.

        Note
        ----
        If you are not able to see the pinned message (eg. you are missing ``READ_MESSAGE_HISTORY``
        and the pinned message is an old message), it will not be returned.
        """
        route = routes.CHANNEL_PINS.compile(self.GET, channel_id=channel_id)
        return await self._request(route)

    async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> None:
        """Add a pinned message to the channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to pin a message to.
        message_id : :obj:`str`
            The ID of the message to pin.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the message or channel do not exist.
        """
        route = routes.CHANNEL_PINS.compile(self.PUT, channel_id=channel_id, message_id=message_id)
        await self._request(route)

    async def delete_pinned_channel_message(self, channel_id: str, message_id: str) -> None:
        """Remove a pinned message from the channel.

        This will only unpin the message, not delete it.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to remove a pin from.
        message_id : :obj:`str`
            The ID of the message to unpin.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_MESSAGES`` permission.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the message or channel do not exist.
        """
        route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id)
        await self._request(route)

    async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get a list of the emojis for a given guild ID.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the emojis for.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of emoji objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you aren't a member of the guild.
        """
        route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str, typing.Any]:
        """Get an emoji from a given guild and emoji IDs.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the emoji from.
        emoji_id : :obj:`str`
            The ID of the emoji to get.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            An emoji object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the emoji aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you aren't a member of said guild.
        """
        route = routes.GUILD_EMOJI.compile(self.GET, guild_id=guild_id, emoji_id=emoji_id)
        return await self._request(route)

    async def create_guild_emoji(
        self, guild_id: str, name: str, image: bytes, *, roles: typing.Sequence[str] = ..., reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a new emoji for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to create the emoji in.
        name : :obj:`str`
            The new emoji's name.
        image : :obj:`bytes`
            The ``128x128`` image in bytes form.
        roles : :obj:`typing.Sequence` [ :obj:`str` ]
            If specified, a list of roles for which the emoji will be whitelisted.
            If empty, all roles are whitelisted.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created emoji object.

        Raises
        ------
        :obj:`ValueError`
            If ``image`` is :obj:`None`.
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format.
        """
        assertions.assert_not_none(image, "image must be a valid image")
        payload = {
            "name": name,
            "roles": [] if roles is ... else roles,
            "image": conversions.image_bytes_to_image_data(image),
        }
        route = routes.GUILD_EMOJIS.compile(self.POST, guild_id=guild_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def modify_guild_emoji(
        self, guild_id: str, emoji_id: str, *, name: str = ..., roles: typing.Sequence[str] = ..., reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edit an emoji of a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to which the emoji to update belongs to.
        emoji_id : :obj:`str`
            The ID of the emoji to update.
        name : :obj:`str`
            If specified, a new emoji name string. Keep unspecified to keep the name the same.
        roles : :obj:`typing.Sequence` [ :obj:`str` ]
            If specified, a list of IDs for the new whitelisted roles.
            Set to an empty list to whitelist all roles.
            Keep unspecified to leave the same roles already set.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The updated emoji object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the emoji aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild.
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "roles", roles)
        route = routes.GUILD_EMOJI.compile(self.PATCH, guild_id=guild_id, emoji_id=emoji_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None:
        """Delete an emoji from a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to delete the emoji from.
        emoji_id : :obj:`str`
            The ID of the emoji to be deleted.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the emoji aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild.
        """
        route = routes.GUILD_EMOJI.compile(self.DELETE, guild_id=guild_id, emoji_id=emoji_id)
        await self._request(route)

    async def create_guild(
        self,
        name: str,
        *,
        region: str = ...,
        icon: bytes = ...,
        verification_level: int = ...,
        default_message_notifications: int = ...,
        explicit_content_filter: int = ...,
        roles: typing.Sequence[typing.Dict[str, typing.Any]] = ...,
        channels: typing.Sequence[typing.Dict[str, typing.Any]] = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a new guild.

        Warning
        -------
        Can only be used by bots in less than ``10`` guilds.

        Parameters
        ----------
        name : :obj:`str`
            The name string for the new guild (``2-100`` characters).
        region : :obj:`str`
            If specified, the voice region ID for new guild. You can use
            :meth:`list_voice_regions` to see which region IDs are available.
        icon : :obj:`bytes`
            If specified, the guild icon image in bytes form.
        verification_level : :obj:`int`
            If specified, the verification level integer (``0-5``).
        default_message_notifications : :obj:`int`
            If specified, the default notification level integer (``0-1``).
        explicit_content_filter : :obj:`int`
            If specified, the explicit content filter integer (``0-2``).
        roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            If specified, an array of role objects to be created alongside the
            guild. First element changes the ``@everyone`` role.
        channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            If specified, an array of channel objects to be created alongside the guild.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created guild object.

        Raises
        ------
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are on ``10`` or more guilds.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide unsupported fields like ``parent_id`` in channel objects.
        """
        payload = {"name": name}
        conversions.put_if_specified(payload, "region", region)
        conversions.put_if_specified(payload, "verification_level", verification_level)
        conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications)
        conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter)
        conversions.put_if_specified(payload, "roles", roles)
        conversions.put_if_specified(payload, "channels", channels)
        conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data)
        route = routes.GUILDS.compile(self.POST)
        return await self._request(route, json_body=payload)

    async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]:
        """Get a given guild's object.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested guild object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you don't have access to the guild.
        """
        route = routes.GUILD.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_guild_preview(self, guild_id: str) -> typing.Dict[str, typing.Any]:
        """Get a public guild's preview object.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the preview object of.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested guild preview object.

        Note
        ----
        Unlike other guild endpoints, the bot doesn't have to be in the target
        guild to get it's preview.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found or it isn't ``PUBLIC``.
        """
        route = routes.GUILD_PREVIEW.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    # pylint: disable=too-many-locals
    async def modify_guild(  # lgtm [py/similar-function]
        self,
        guild_id: str,
        *,
        name: str = ...,
        region: str = ...,
        verification_level: int = ...,
        default_message_notifications: int = ...,
        explicit_content_filter: int = ...,
        afk_channel_id: str = ...,
        afk_timeout: int = ...,
        icon: bytes = ...,
        owner_id: str = ...,
        splash: bytes = ...,
        system_channel_id: str = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edit a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to be edited.
        name : :obj:`str`
            If specified, the new name string for the guild (``2-100`` characters).
        region : :obj:`str`
            If specified, the new voice region ID for guild. You can use
            :meth:`list_voice_regions` to see which region IDs are available.
        verification_level : :obj:`int`
            If specified, the new verification level integer (``0-5``).
        default_message_notifications : :obj:`int`
            If specified, the new default notification level integer (``0-1``).
        explicit_content_filter : :obj:`int`
            If specified, the new explicit content filter integer (``0-2``).
        afk_channel_id : :obj:`str`
            If specified, the new ID for the AFK voice channel.
        afk_timeout : :obj:`int`
            If specified, the new AFK timeout period in seconds
        icon : :obj:`bytes`
            If specified, the new guild icon image in bytes form.
        owner_id : :obj:`str`
            If specified, the new ID of the new guild owner.
        splash : :obj:`bytes`
            If specified, the new new splash image in bytes form.
        system_channel_id : :obj:`str`
            If specified, the new ID of the new system channel.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The edited guild object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "region", region)
        conversions.put_if_specified(payload, "verification_level", verification_level)
        conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications)
        conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter)
        conversions.put_if_specified(payload, "afk_channel_id", afk_channel_id)
        conversions.put_if_specified(payload, "afk_timeout", afk_timeout)
        conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data)
        conversions.put_if_specified(payload, "owner_id", owner_id)
        conversions.put_if_specified(payload, "splash", splash, conversions.image_bytes_to_image_data)
        conversions.put_if_specified(payload, "system_channel_id", system_channel_id)
        route = routes.GUILD.compile(self.PATCH, guild_id=guild_id)
        return await self._request(route, json_body=payload, reason=reason)

    # pylint: enable=too-many-locals

    async def delete_guild(self, guild_id: str) -> None:
        """Permanently delete the given guild.

        You must be owner of the guild to perform this action.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to be deleted.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not the guild owner.
        """
        route = routes.GUILD.compile(self.DELETE, guild_id=guild_id)
        await self._request(route)

    async def list_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get all the channels for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the channels from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of channel objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not in the guild.
        """
        route = routes.GUILD_CHANNELS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def create_guild_channel(
        self,
        guild_id: str,
        name: str,
        *,
        type_: int = ...,
        position: int = ...,
        topic: str = ...,
        nsfw: bool = ...,
        rate_limit_per_user: int = ...,
        bitrate: int = ...,
        user_limit: int = ...,
        permission_overwrites: typing.Sequence[typing.Dict[str, typing.Any]] = ...,
        parent_id: str = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a channel in a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to create the channel in.
        name : :obj:`str`
            If specified, the name for the channel.This must be
            between ``2`` and ``100`` characters in length.
        type_: :obj:`int`
            If specified, the channel type integer (``0-6``).
        position : :obj:`int`
            If specified, the position to change the channel to.
        topic : :obj:`str`
            If specified, the topic to set. This is only applicable to
            text channels. This must be between ``0`` and ``1024``
            characters in length.
        nsfw : :obj:`bool`
            If specified, whether the channel will be marked as NSFW.
            Only applicable to text channels.
        rate_limit_per_user : :obj:`int`
            If specified, the number of seconds the user has to wait before sending
            another message.  This will not apply to bots, or to members with
            ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must
            be between ``0`` and ``21600`` seconds.
        bitrate : :obj:`int`
            If specified, the bitrate in bits per second allowable for the channel.
            This only applies to voice channels and must be between ``8000``
            and ``96000`` for normal servers or ``8000`` and ``128000`` for
            VIP servers.
        user_limit : :obj:`int`
            If specified, the max number of users to allow in a voice channel.
            This must be between ``0`` and ``99`` inclusive, where
            ``0`` implies no limit.
        permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            If specified, the list of permission overwrites that are category
            specific to replace the existing overwrites with.
        parent_id : :obj:`str`
            If specified, the parent category ID to set for the channel.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created channel object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide incorrect options for the corresponding channel type
            (e.g. a ``bitrate`` for a text channel).
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "type", type_)
        conversions.put_if_specified(payload, "position", position)
        conversions.put_if_specified(payload, "topic", topic)
        conversions.put_if_specified(payload, "nsfw", nsfw)
        conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user)
        conversions.put_if_specified(payload, "bitrate", bitrate)
        conversions.put_if_specified(payload, "user_limit", user_limit)
        conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites)
        conversions.put_if_specified(payload, "parent_id", parent_id)
        route = routes.GUILD_CHANNELS.compile(self.POST, guild_id=guild_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def modify_guild_channel_positions(
        self, guild_id: str, channel: typing.Tuple[str, int], *channels: typing.Tuple[str, int]
    ) -> None:
        """Edit the position of one or more given channels.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild in which to edit the channels.
        channel : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ]
            The first channel to change the position of. This is a tuple of the channel ID and the integer position.
        *channels : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ]
            Optional additional channels to change the position of. These must be tuples of the channel ID and the
            integer positions to change to.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or any of the channels aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in
            the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide anything other than the ``id`` and ``position`` fields for the channels.
        """
        payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)]
        route = routes.GUILD_CHANNELS.compile(self.PATCH, guild_id=guild_id)
        await self._request(route, json_body=payload)

    async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]:
        """Get a given guild member.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the member from.
        user_id : :obj:`str`
            The ID of the member to get.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested member object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the member aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you don't have access to the target guild.
        """
        route = routes.GUILD_MEMBER.compile(self.GET, guild_id=guild_id, user_id=user_id)
        return await self._request(route)

    async def list_guild_members(
        self, guild_id: str, *, limit: int = ..., after: str = ...,
    ) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """List all members of a given guild.

        Parameters
        ----------
            guild_id : :obj:`str`
                The ID of the guild to get the members from.
            limit : :obj:`int`
                If specified, the maximum number of members to return. This has to be between
                ``1`` and ``1000`` inclusive.
            after : :obj:`str`
                If specified, the highest ID in the previous page. This is used for retrieving more
                than ``1000`` members in a server using consecutive requests.

        Example
        -------
        .. code-block:: python

            members = []
            last_id = 0

            while True:
                next_members = await client.list_guild_members(1234567890, limit=1000, after=last_id)
                members += next_members

                if len(next_members) == 1000:
                    last_id = next_members[-1]["user"]["id"]
                else:
                    break

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            A list of member objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide invalid values for the ``limit`` or `after`` fields.
        """
        query = {}
        conversions.put_if_specified(query, "limit", limit)
        conversions.put_if_specified(query, "after", after)
        route = routes.GUILD_MEMBERS.compile(self.GET, guild_id=guild_id)
        return await self._request(route, query=query)

    async def modify_guild_member(  # lgtm [py/similar-function]
        self,
        guild_id: str,
        user_id: str,
        *,
        nick: typing.Optional[str] = ...,
        roles: typing.Sequence[str] = ...,
        mute: bool = ...,
        deaf: bool = ...,
        channel_id: typing.Optional[str] = ...,
        reason: str = ...,
    ) -> None:
        """Edit a member of a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to edit the member from.
        user_id : :obj:`str`
            The ID of the member to edit.
        nick : :obj:`str`, optional
            If specified, the new nickname string. Setting it to :obj:`None`
            explicitly will clear the nickname.
        roles : :obj:`typing.Sequence` [ :obj:`str` ]
            If specified, a list of role IDs the member should have.
        mute : :obj:`bool`
            If specified, whether the user should be muted in the voice channel
            or not.
        deaf : :obj:`bool`
            If specified, whether the user should be deafen in the voice channel
            or not.
        channel_id : :obj:`str`
            If specified, the ID of the channel to move the member to. Setting
            it to :obj:`None` explicitly will disconnect the user.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild, user, channel or any of the roles aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack any of the applicable permissions
            (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``).
            Note that to move a member you must also have permission to connect to the end channel.
            This will also be raised if you're not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel.
        """
        payload = {}
        conversions.put_if_specified(payload, "nick", nick)
        conversions.put_if_specified(payload, "roles", roles)
        conversions.put_if_specified(payload, "mute", mute)
        conversions.put_if_specified(payload, "deaf", deaf)
        conversions.put_if_specified(payload, "channel_id", channel_id)
        route = routes.GUILD_MEMBER.compile(self.PATCH, guild_id=guild_id, user_id=user_id)
        await self._request(route, json_body=payload, reason=reason)

    async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...,) -> None:
        """Edit the current user's nickname for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to change the nick on.
        nick : :obj:`str`, optional
            The new nick string. Setting this to `None` clears the nickname.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide a disallowed nickname, one that is too long, or one that is empty.
        """
        payload = {"nick": nick}
        route = routes.OWN_GUILD_NICKNAME.compile(self.PATCH, guild_id=guild_id)
        await self._request(route, json_body=payload, reason=reason)

    async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None:
        """Add a role to a given member.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild the member belongs to.
        user_id : :obj:`str`
            The ID of the member you want to add the role to.
        role_id : :obj:`str`
            The ID of the role you want to add.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild, member or role aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or are not in the guild.
        """
        route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id)
        await self._request(route, reason=reason)

    async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None:
        """Remove a role from a given member.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild the member belongs to.
        user_id : :obj:`str`
            The ID of the member you want to remove the role from.
        role_id : :obj:`str`
            The ID of the role you want to remove.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild, member or role aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or are not in the guild.
        """
        route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id)
        await self._request(route, reason=reason)

    async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None:
        """Kick a user from a given guild.

        Parameters
        ----------
        guild_id: :obj:`str`
            The ID of the guild the member belongs to.
        user_id: :obj:`str`
            The ID of the member you want to kick.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or member aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``KICK_MEMBERS`` permission or are not in the guild.
        """
        route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id)
        await self._request(route, reason=reason)

    async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the bans for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to get the bans from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of ban objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``BAN_MEMBERS`` permission or are not in the guild.
        """
        route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]:
        """Get a ban from a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to get the ban from.
        user_id : :obj:`str`
            The ID of the user to get the ban information for.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            A ban object for the requested user.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the user aren't found, or if the user is not banned.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``BAN_MEMBERS`` permission or are not in the guild.
        """
        route = routes.GUILD_BAN.compile(self.GET, guild_id=guild_id, user_id=user_id)
        return await self._request(route)

    async def create_guild_ban(
        self, guild_id: str, user_id: str, *, delete_message_days: int = ..., reason: str = ...,
    ) -> None:
        """Ban a user from a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild the member belongs to.
        user_id : :obj:`str`
            The ID of the member you want to ban.
        delete_message_days : :obj:`str`
            If specified, how many days of messages from the user should
            be removed.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or member aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``BAN_MEMBERS`` permission or are not in the guild.
        """
        query = {}
        conversions.put_if_specified(query, "delete-message-days", delete_message_days)
        conversions.put_if_specified(query, "reason", reason)
        route = routes.GUILD_BAN.compile(self.PUT, guild_id=guild_id, user_id=user_id)
        await self._request(route, query=query)

    async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None:
        """Un-bans a user from a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to un-ban the user from.
        user_id : :obj:`str`
            The ID of the user you want to un-ban.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or member aren't found, or the member is not banned.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``BAN_MEMBERS`` permission or are not a in the guild.
        """
        route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id)
        await self._request(route, reason=reason)

    async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the roles for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to get the roles from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of role objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you're not in the guild.
        """
        route = routes.GUILD_ROLES.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def create_guild_role(
        self,
        guild_id: str,
        *,
        name: str = ...,
        permissions: int = ...,
        color: int = ...,
        hoist: bool = ...,
        mentionable: bool = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a new role for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to create the role on.
        name : :obj:`str`
            If specified, the new role name string.
        permissions : :obj:`int`
            If specified, the permissions integer for the role.
        color : :obj:`int`
            If specified, the color for the role.
        hoist : :obj:`bool`
            If specified, whether the role will be hoisted.
        mentionable : :obj:`bool`
           If specified, whether the role will be able to be mentioned by any user.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created role object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or you're not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide invalid values for the role attributes.
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "permissions", permissions)
        conversions.put_if_specified(payload, "color", color)
        conversions.put_if_specified(payload, "hoist", hoist)
        conversions.put_if_specified(payload, "mentionable", mentionable)
        route = routes.GUILD_ROLES.compile(self.POST, guild_id=guild_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def modify_guild_role_positions(
        self, guild_id: str, role: typing.Tuple[str, int], *roles: typing.Tuple[str, int]
    ) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Edit the position of two or more roles in a given guild.

        Parameters
        ----------
        guild_id : str
            The ID of the guild the roles belong to.
        role : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ]
            The first role to move. This is a tuple of the role ID and the
            integer position.
        *roles : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ]
            Optional extra roles to move. These must be tuples of the role ID
            and the integer position.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of all the guild roles.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or any of the roles aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or you're not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide invalid values for the `position` fields.
        """
        payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)]
        route = routes.GUILD_ROLES.compile(self.PATCH, guild_id=guild_id)
        return await self._request(route, json_body=payload)

    async def modify_guild_role(  # lgtm [py/similar-function]
        self,
        guild_id: str,
        role_id: str,
        *,
        name: str = ...,
        permissions: int = ...,
        color: int = ...,
        hoist: bool = ...,
        mentionable: bool = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edits a role in a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild the role belong to.
        role_id : :obj:`str`
            The ID of the role you want to edit.
        name : :obj:`str`
            If specified, the new role's name string.
        permissions : :obj:`int`
            If specified, the new permissions integer for the role.
        color : :obj:`int`
            If specified, the new color for the new role.
        hoist : :obj:`bool`
            If specified, whether the role should hoist or not.
        mentionable : :obj:`bool`
            If specified, whether the role should be mentionable or not.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The edited role object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or role aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or you're not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide invalid values for the role attributes.
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "permissions", permissions)
        conversions.put_if_specified(payload, "color", color)
        conversions.put_if_specified(payload, "hoist", hoist)
        conversions.put_if_specified(payload, "mentionable", mentionable)
        route = routes.GUILD_ROLE.compile(self.PATCH, guild_id=guild_id, role_id=role_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def delete_guild_role(self, guild_id: str, role_id: str) -> None:
        """Delete a role from a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to remove the role from.
        role_id : :obj:`str`
            The ID of the role you want to delete.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the role aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_ROLES`` permission or are not in the guild.
        """
        route = routes.GUILD_ROLE.compile(self.DELETE, guild_id=guild_id, role_id=role_id)
        await self._request(route)

    async def get_guild_prune_count(self, guild_id: str, days: int) -> int:
        """Get the estimated prune count for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to get the count for.
        days : :obj:`int`
            The number of days to count prune for (at least ``1``).

        Returns
        -------
        :obj:`int`
            The number of members estimated to be pruned.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``KICK_MEMBERS`` or you are not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you pass an invalid amount of days.
        """
        payload = {"days": days}
        route = routes.GUILD_PRUNE.compile(self.GET, guild_id=guild_id)
        result = await self._request(route, query=payload)
        return int(result["pruned"])

    async def begin_guild_prune(
        self, guild_id: str, days: int, *, compute_prune_count: bool = ..., reason: str = ...,
    ) -> typing.Optional[int]:
        """Prune members of a given guild based on the number of inactive days.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild you want to prune member of.
        days : :obj:`int`
            The number of inactivity days you want to use as filter.
        compute_prune_count : :obj:`bool`
            Whether a count of pruned members is returned or not.
            Discouraged for large guilds out of politeness.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`int`, optional
            The number of members who were kicked if ``compute_prune_count``
            is :obj:`True`, else :obj:`None`.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found:
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``KICK_MEMBER`` permission or are not in the guild.
        :obj:`hikari.errors.BadRequestHTTPError`
            If you provide invalid values for the ``days`` or ``compute_prune_count`` fields.
        """
        query = {"days": days}
        conversions.put_if_specified(query, "compute_prune_count", compute_prune_count, str)
        route = routes.GUILD_PRUNE.compile(self.POST, guild_id=guild_id)
        result = await self._request(route, query=query, reason=reason)

        try:
            return int(result["pruned"])
        except (TypeError, KeyError):
            return None

    async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the voice regions for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the voice regions for.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of voice region objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you are not in the guild.
        """
        route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the invites for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the invites for.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of invite objects (with metadata).

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the integrations for a given guild.

        Parameters
        ----------
        guild_id : :obj:`int`
            The ID of the guild to get the integrations for.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of integration objects.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def modify_guild_integration(
        self,
        guild_id: str,
        integration_id: str,
        *,
        expire_behaviour: int = ...,
        expire_grace_period: int = ...,
        enable_emojis: bool = ...,
        reason: str = ...,
    ) -> None:
        """Edit an integrations for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to which the integration belongs to.
        integration_id : :obj:`str`
            The ID of the integration.
        expire_behaviour : :obj:`int`
            If specified, the behaviour for when an integration subscription
            lapses.
        expire_grace_period : :obj:`int`
            If specified, time interval in seconds in which the integration
            will ignore lapsed subscriptions.
        enable_emojis : :obj:`bool`
            If specified, whether emojis should be synced for this integration.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the integration aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        payload = {}
        conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour)
        conversions.put_if_specified(payload, "expire_grace_period", expire_grace_period)
        # This is inconsistently named in their API.
        conversions.put_if_specified(payload, "enable_emoticons", enable_emojis)
        route = routes.GUILD_INTEGRATION.compile(self.PATCH, guild_id=guild_id, integration_id=integration_id)
        await self._request(route, json_body=payload, reason=reason)

    async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...) -> None:
        """Delete an integration for the given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to which the integration belongs to.
        integration_id : :obj:`str`
            The ID of the integration to delete.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
                If either the guild or the integration aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
                If you lack the `MANAGE_GUILD` permission or are not in the guild.
        """
        route = routes.GUILD_INTEGRATION.compile(self.DELETE, guild_id=guild_id, integration_id=integration_id)
        await self._request(route, reason=reason)

    async def sync_guild_integration(self, guild_id: str, integration_id: str) -> None:
        """Sync the given integration.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to which the integration belongs to.
        integration_id : :obj:`str`
            The ID of the integration to sync.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the guild or the integration aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id)
        await self._request(route)

    async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]:
        """Get the embed for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the embed for.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            A guild embed object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        route = routes.GUILD_EMBED.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def modify_guild_embed(
        self, guild_id: str, *, channel_id: typing.Optional[str] = ..., enabled: bool = ..., reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edit the embed for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to edit the embed for.
        channel_id : :obj:`str`, optional
            If specified, the channel that this embed's invite should target.
            Set to :obj:`None` to disable invites for this embed.
        enabled : :obj:`bool`
            If specified, whether this embed should be enabled.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The updated embed object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        payload = {}
        conversions.put_if_specified(payload, "channel_id", channel_id)
        conversions.put_if_specified(payload, "enabled", enabled)
        route = routes.GUILD_EMBED.compile(self.PATCH, guild_id=guild_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.Any]:
        """Get the vanity URL for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to get the vanity URL for.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            A partial invite object containing the vanity URL in the ``code`` field.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_GUILD`` permission or are not in the guild.
        """
        route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str:
        """Get the URL for a guild widget.

        Parameters
        ----------
        guild_id : :obj:`str`
            The guild ID to use for the widget.
        style : :obj:`str`
            If specified, the syle of the widget.

        Returns
        -------
        :obj:`str`
            A URL to retrieve a PNG widget for your guild.

        Note
        ----
        This does not actually make any form of request, and shouldn't be awaited.
        Thus, it doesn't have rate limits either.

        Warning
        -------
        The guild must have the widget enabled in the guild settings for this to be valid.
        """
        query = "" if style is ... else f"?style={style}"
        return f"{self.base_url}/guilds/{guild_id}/widget.png" + query

    async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typing.Dict[str, typing.Any]:
        """Getsthe given invite.

        Parameters
        ----------
        invite_code : :obj:`str`
            The ID for wanted invite.
        with_counts : :obj:`bool`
            If specified, whether to attempt to count the number of
            times the invite has been used.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested invite object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the invite is not found.
        """
        query = {}
        conversions.put_if_specified(query, "with_counts", with_counts, str)
        route = routes.INVITE.compile(self.GET, invite_code=invite_code)
        return await self._request(route, query=query)

    async def delete_invite(self, invite_code: str) -> None:
        """Delete a given invite.

        Parameters
        ----------
        invite_code : :obj:`str`
            The ID for the invite to be deleted.

        Returns
        -------
        :obj:`None` # Marker
            Nothing, unlike what the API specifies. This is done to maintain
            consistency with other calls of a similar nature in this API wrapper.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the invite is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack either ``MANAGE_CHANNELS`` on the channel the invite
            belongs to or ``MANAGE_GUILD`` for guild-global delete.
        """
        route = routes.INVITE.compile(self.DELETE, invite_code=invite_code)
        return await self._request(route)

    async def get_current_user(self) -> typing.Dict[str, typing.Any]:
        """Get the current user that is represented by token given to the client.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The current user object.
        """
        route = routes.OWN_USER.compile(self.GET)
        return await self._request(route)

    async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]:
        """Get a given user.

        Parameters
        ----------
        user_id : :obj:`str`
            The ID of the user to get.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested user object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the user is not found.
        """
        route = routes.USER.compile(self.GET, user_id=user_id)
        return await self._request(route)

    async def modify_current_user(
        self, *, username: str = ..., avatar: typing.Optional[bytes] = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edit the current user.

        Parameters
        ----------
        username : :obj:`str`
            If specified, the new username string.
        avatar : :obj:`bytes`, optional
            If specified, the new avatar image in bytes form.
            If it is :obj:`None`, the avatar is removed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The updated user object.

        Raises
        ------
        :obj:`hikari.errors.BadRequestHTTPError`
            If you pass username longer than the limit (``2-32``) or an invalid image.
        """
        payload = {}
        conversions.put_if_specified(payload, "username", username)
        conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data)
        route = routes.OWN_USER.compile(self.PATCH)
        return await self._request(route, json_body=payload)

    async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the current user's connections.

        This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but
        will usually return an empty list for bots (with there being some exceptions
        to this, like user accounts that have been converted to bots).

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of connection objects.
        """
        route = routes.OWN_CONNECTIONS.compile(self.GET)
        return await self._request(route)

    async def get_current_user_guilds(
        self, *, before: str = ..., after: str = ..., limit: int = ...,
    ) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the guilds the current user is in.

        Parameters
        ----------
        before : :obj:`str`
            If specified, the guild ID to get guilds before it.

        after : :obj:`str`
            If specified, the guild ID to get guilds after it.

        limit : :obj:`int`
            If specified, the limit of guilds to get. Has to be between
            ``1`` and ``100``.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of partial guild objects.

        Raises
        ------
        :obj:`hikari.errors.BadRequestHTTPError`
            If you pass both ``before`` and ``after`` or an
            invalid value for ``limit``.
        """
        query = {}
        conversions.put_if_specified(query, "before", before)
        conversions.put_if_specified(query, "after", after)
        conversions.put_if_specified(query, "limit", limit)
        route = routes.OWN_GUILDS.compile(self.GET)
        return await self._request(route, query=query)

    async def leave_guild(self, guild_id: str) -> None:
        """Make the current user leave a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID of the guild to leave.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        """
        route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id)
        await self._request(route)

    async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]:
        """Create a new DM channel with a given user.

        Parameters
        ----------
        recipient_id : :obj:`str`
            The ID of the user to create the new DM channel with.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created DM channel object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the recipient is not found.
        """
        payload = {"recipient_id": recipient_id}
        route = routes.OWN_DMS.compile(self.POST)
        return await self._request(route, json_body=payload)

    async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get the voice regions that are available.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of voice regions available

        Note
        ----
        This does not include VIP servers.
        """
        route = routes.VOICE_REGIONS.compile(self.GET)
        return await self._request(route)

    async def create_webhook(
        self, channel_id: str, name: str, *, avatar: bytes = ..., reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Create a webhook for a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel for webhook to be created in.
        name : :obj:`str`
            The webhook's name string.
        avatar : :obj:`bytes`
            If specified, the avatar image in bytes form.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The newly created webhook object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_WEBHOOKS`` permission or
            can not see the given channel.
        :obj:`hikari.errors.BadRequestHTTPError`
            If the avatar image is too big or the format is invalid.
        """
        payload = {"name": name}
        conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data)
        route = routes.CHANNEL_WEBHOOKS.compile(self.POST, channel_id=channel_id)
        return await self._request(route, json_body=payload, reason=reason)

    async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get all webhooks from a given channel.

        Parameters
        ----------
        channel_id : :obj:`str`
            The ID of the channel to get the webhooks from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of webhook objects for the give channel.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_WEBHOOKS`` permission or
            can not see the given channel.
        """
        route = routes.CHANNEL_WEBHOOKS.compile(self.GET, channel_id=channel_id)
        return await self._request(route)

    async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]:
        """Get all webhooks for a given guild.

        Parameters
        ----------
        guild_id : :obj:`str`
            The ID for the guild to get the webhooks from.

        Returns
        -------
        :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ]
            A list of webhook objects for the given guild.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the guild is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_WEBHOOKS`` permission or
            aren't a member of the given guild.
        """
        route = routes.GUILD_WEBHOOKS.compile(self.GET, guild_id=guild_id)
        return await self._request(route)

    async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typing.Dict[str, typing.Any]:
        """Get a given webhook.

        Parameters
        ----------
        webhook_id : :obj:`str`
            The ID of the webhook to get.
        webhook_token : :obj:`str`
            If specified, the webhook token to use to get it (bypassing bot authorization).

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The requested webhook object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the webhook is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you're not in the guild that owns this webhook or
            lack the ``MANAGE_WEBHOOKS`` permission.
        :obj:`hikari.errors.UnauthorizedHTTPError`
            If you pass a token that's invalid for the target webhook.
        """
        if webhook_token is ...:
            route = routes.WEBHOOK.compile(self.GET, webhook_id=webhook_id)
        else:
            route = routes.WEBHOOK_WITH_TOKEN.compile(self.GET, webhook_id=webhook_id, webhook_token=webhook_token)
        return await self._request(route, suppress_authorization_header=webhook_token is not ...)

    async def modify_webhook(
        self,
        webhook_id: str,
        *,
        webhook_token: str = ...,
        name: str = ...,
        avatar: typing.Optional[bytes] = ...,
        channel_id: str = ...,
        reason: str = ...,
    ) -> typing.Dict[str, typing.Any]:
        """Edit a given webhook.

        Parameters
        ----------
        webhook_id : :obj:`str`
            The ID of the webhook to edit.
        webhook_token : :obj:`str`
            If specified, the webhook token to use to modify it (bypassing bot authorization).
        name : :obj:`str`
            If specified, the new name string.
        avatar : :obj:`bytes`
            If specified, the new avatar image in bytes form. If None, then
            it is removed.
        channel_id : :obj:`str`
            If specified, the ID of the new channel the given
            webhook should be moved to.
        reason : :obj:`str`
            If specified, the audit log reason explaining why the operation
            was performed.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            The updated webhook object.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If either the webhook or the channel aren't found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_WEBHOOKS`` permission or
            aren't a member of the guild this webhook belongs to.
        :obj:`hikari.errors.UnauthorizedHTTPError`
            If you pass a token that's invalid for the target webhook.
        """
        payload = {}
        conversions.put_if_specified(payload, "name", name)
        conversions.put_if_specified(payload, "channel_id", channel_id)
        conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data)
        if webhook_token is ...:
            route = routes.WEBHOOK.compile(self.PATCH, webhook_id=webhook_id)
        else:
            route = routes.WEBHOOK_WITH_TOKEN.compile(self.PATCH, webhook_id=webhook_id, webhook_token=webhook_token)
        return await self._request(
            route, json_body=payload, reason=reason, suppress_authorization_header=webhook_token is not ...,
        )

    async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> None:
        """Delete a given webhook.

        Parameters
        ----------
        webhook_id : :obj:`str`
            The ID of the webhook to delete
        webhook_token : :obj:`str`
            If specified, the webhook token to use to
            delete it (bypassing bot authorization).

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the webhook is not found.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you either lack the ``MANAGE_WEBHOOKS`` permission or
            aren't a member of the guild this webhook belongs to.
        :obj:`hikari.errors.UnauthorizedHTTPError`
            If you pass a token that's invalid for the target webhook.
        """
        if webhook_token is ...:
            route = routes.WEBHOOK.compile(self.DELETE, webhook_id=webhook_id)
        else:
            route = routes.WEBHOOK_WITH_TOKEN.compile(self.DELETE, webhook_id=webhook_id, webhook_token=webhook_token)
        await self._request(route, suppress_authorization_header=webhook_token is not ...)

    async def execute_webhook(
        self,
        webhook_id: str,
        webhook_token: str,
        *,
        content: str = ...,
        username: str = ...,
        avatar_url: str = ...,
        tts: bool = ...,
        wait: bool = ...,
        file: typing.Tuple[str, conversions.FileLikeT] = ...,
        embeds: typing.Sequence[typing.Dict[str, typing.Any]] = ...,
        allowed_mentions: typing.Dict[str, typing.Any] = ...,
    ) -> typing.Optional[typing.Dict[str, typing.Any]]:
        """Execute a webhook to create a message in its channel.

        Parameters
        ----------
        webhook_id : :obj:`str`
            The ID of the webhook to execute.
        webhook_token : :obj:`str`
            The token of the webhook to execute.
        content : :obj:`str`
            If specified, the webhook message content to send.
        username : :obj:`str`
            If specified, the username to override the webhook's username
            for this request.
        avatar_url : :obj:`str`
            If specified, the url of an image to override the webhook's
            avatar with for this request.
        tts : :obj:`bool`
            If specified, whether this webhook should create a TTS message.
        wait : :obj:`bool`
            If specified, whether this request should wait for the webhook
            to be executed and return the resultant message object.
        file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ]
            If specified, a tuple of the file name and either raw :obj:`bytes`
            or a :obj:`io.IOBase` derived object that points to a buffer
            containing said file.
        embeds : :obj:`typing.Sequence` [:obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]]
            If specified, the sequence of embed objects that will be sent
            with this message.
        allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            If specified, the mentions to parse from the ``content``.
            If not specified, will parse all mentions from the ``content``.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional
            The created message object if ``wait`` is :obj:`True`, else
            :obj:`None`.

        Raises
        ------
        :obj:`hikari.errors.NotFoundHTTPError`
            If the channel ID or webhook ID is not found.
        :obj:`hikari.errors.BadRequestHTTPError`
            This can be raised if the file is too large; if the embed exceeds
            the defined limits; if the message content is specified only and
            empty or greater than ``2000`` characters; if neither content, file
            or embed are specified; if there is a duplicate id in only of the
            fields in ``allowed_mentions``; if you specify to parse all
            users/roles mentions but also specify which users/roles to parse
            only.
        :obj:`hikari.errors.ForbiddenHTTPError`
            If you lack permissions to send to this channel.
        :obj:`hikari.errors.UnauthorizedHTTPError`
            If you pass a token that's invalid for the target webhook.
        """
        form = aiohttp.FormData()

        json_payload = {}
        conversions.put_if_specified(json_payload, "content", content)
        conversions.put_if_specified(json_payload, "username", username)
        conversions.put_if_specified(json_payload, "avatar_url", avatar_url)
        conversions.put_if_specified(json_payload, "tts", tts)
        conversions.put_if_specified(json_payload, "embeds", embeds)
        conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions)

        form.add_field("payload_json", json.dumps(json_payload), content_type="application/json")

        if file is not ...:
            file_name, file = file
            file = conversions.make_resource_seekable(file)
            re_seekable_resources = [file]
            form.add_field("file", file, filename=file_name, content_type="application/octet-stream")
        else:
            re_seekable_resources = []

        query = {}
        conversions.put_if_specified(query, "wait", wait, str)

        route = routes.WEBHOOK_WITH_TOKEN.compile(self.POST, webhook_id=webhook_id, webhook_token=webhook_token)
        return await self._request(
            route,
            form_body=form,
            re_seekable_resources=re_seekable_resources,
            query=query,
            suppress_authorization_header=True,
        )

    ##########
    # OAUTH2 #
    ##########

    async def get_current_application_info(self) -> typing.Dict[str, typing.Any]:
        """Get the current application information.

        Returns
        -------
        :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]
            An application info object.
        """
        route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET)
        return await self._request(route)
