Exemple #1
0
    def __init__(self,
                 token: str,
                 *,
                 state_klass: type = None,
                 bot_type: int = (BotType.BOT | BotType.ONLY_USER)):
        """
        :param token: The current token for this bot.
        :param state_klass: The class to construct the connection state from.
        :param bot_type: A union of :class:`.BotType` that defines the type of this bot.
        """
        #: The mapping of `shard_id -> gateway` objects.
        self._gateways = {}  # type: typing.MutableMapping[int, GatewayHandler]

        #: The number of shards this client has.
        self.shard_count = 0

        #: The token for the bot.
        self._token = token

        if state_klass is None:
            from curious.core.state import State
            state_klass = State

        #: The current connection state for the bot.
        self.state = state_klass(self)

        #: The bot type for this bot.
        self.bot_type = bot_type

        if self.bot_type & BotType.BOT and self.bot_type & BotType.USERBOT:
            raise ValueError(
                "Bot cannot be a bot and a userbot at the same time")

        #: The current :class:`.EventManager` for this bot.
        self.events = EventManager()
        #: The current :class:`.Chunker` for this bot.
        self.chunker = md_chunker.Chunker(self)
        self.chunker.register_events(self.events)

        self._ready_state = {}

        #: The :class:`.HTTPClient` used for this bot.
        self.http = HTTPClient(self._token,
                               bot=bool(self.bot_type & BotType.BOT))

        #: The cached gateway URL.
        self._gw_url = None  # type: str

        #: The application info for this bot. Instance of :class:`.AppInfo`.
        #: This will be None for user bots.
        self.application_info = None  # type: AppInfo

        #: The task manager used for this bot.
        self.task_manager = None

        for (name, event) in scan_events(self):
            self.events.add_event(event)
Exemple #2
0
    def __init__(self,
                 token: str,
                 *,
                 state_klass: type = None,
                 bot_type: int = (BotType.BOT | BotType.ONLY_USER)):
        """
        :param token: The current token for this bot.
        :param state_klass: The class to construct the connection state from.
        :param bot_type: A union of :class:`~.BotType` that defines the type of this bot.
        """
        #: The mapping of `shard_id -> gateway` objects.
        self._gateways = {}

        #: The number of shards this client has.
        self.shard_count = 0

        #: The token for the bot.
        self._token = token

        if state_klass is None:
            from curious.core.state import State
            state_klass = State

        #: The current connection state for the bot.
        self.state = state_klass(self)

        #: The bot type for this bot.
        self.bot_type = bot_type

        if self.bot_type & BotType.BOT and self.bot_type & BotType.USERBOT:
            raise ValueError(
                "Bot cannot be a bot and a userbot at the same time")

        #: The current :class:`.EventManager` for this bot.
        self.events = EventManager()
        self.events.add_event(self.handle_dispatches,
                              name="gateway_dispatch_received")

        #: The :class:`~.HTTPClient` used for this bot.
        self.http = HTTPClient(self._token,
                               bot=bool(self.bot_type & BotType.BOT))

        #: The cached gateway URL.
        self._gw_url = None  # type: str

        #: The application info for this bot. Instance of :class:`~.AppInfo`.
        #: This will be None for user bots.
        self.application_info = None  # type: AppInfo

        self.scan_events()
Exemple #3
0
class Client(object):
    """
    The main client class. This is used to interact with Discord.

    To start, you can create an instance of the client by passing it the token you want to use:

    .. code-block:: python3

        cl = Client("my.token.string")

    Registering events can be done with the :meth:`.Client.event` decorator, or alternatively
    manual usage of the :class:`.EventHandler` on :attr:`.Client.events`.

    .. code-block:: python3

        @cl.event("ready")
        async def loaded(ctx: EventContext):
            print("Bot logged in.")

    """
    #: A list of events to ignore the READY status.
    IGNORE_READY = [
        "connect", "guild_streamed", "guild_chunk", "guild_available",
        "guild_sync"
    ]

    def __init__(self,
                 token: str,
                 *,
                 state_klass: type = None,
                 bot_type: int = (BotType.BOT | BotType.ONLY_USER)):
        """
        :param token: The current token for this bot.
        :param state_klass: The class to construct the connection state from.
        :param bot_type: A union of :class:`.BotType` that defines the type of this bot.
        """
        #: The mapping of `shard_id -> gateway` objects.
        self._gateways = {}  # type: typing.MutableMapping[int, GatewayHandler]

        #: The number of shards this client has.
        self.shard_count = 0

        #: The token for the bot.
        self._token = token

        if state_klass is None:
            from curious.core.state import State
            state_klass = State

        #: The current connection state for the bot.
        self.state = state_klass(self)

        #: The bot type for this bot.
        self.bot_type = bot_type

        if self.bot_type & BotType.BOT and self.bot_type & BotType.USERBOT:
            raise ValueError(
                "Bot cannot be a bot and a userbot at the same time")

        #: The current :class:`.EventManager` for this bot.
        self.events = EventManager()
        #: The current :class:`.Chunker` for this bot.
        self.chunker = md_chunker.Chunker(self)
        self.chunker.register_events(self.events)

        self._ready_state = {}

        #: The :class:`.HTTPClient` used for this bot.
        self.http = HTTPClient(self._token,
                               bot=bool(self.bot_type & BotType.BOT))

        #: The cached gateway URL.
        self._gw_url = None  # type: str

        #: The application info for this bot. Instance of :class:`.AppInfo`.
        #: This will be None for user bots.
        self.application_info = None  # type: AppInfo

        #: The task manager used for this bot.
        self.task_manager = None

        for (name, event) in scan_events(self):
            self.events.add_event(event)

    @property
    def user(self) -> BotUser:
        """
        :return: The :class:`.User` that this client is logged in as.
        """
        return self.state._user

    @property
    def guilds(self) -> 'typing.Mapping[int, dt_guild.Guild]':
        """
        :return: A mapping of int -> :class:`.Guild` that this client can see.
        """
        return self.state.guilds

    @property
    def invite_url(self) -> str:
        """
        :return: The invite URL for this bot.
        """
        return "https://discordapp.com/oauth2/authorize?client_id={}&scope=bot".format(
            self.application_info.client_id)

    @property
    def events_handled(self) -> collections.Counter:
        """
        A :class:`collections.Counter` of all events that have been handled since the bot's bootup.
        This can be used to track statistics for events.
         
        .. code-block:: python3
        
            @command()
            async def events(self, ctx: Context):
                '''
                Shows the most common events.
                '''
                
                ev = ctx.bot.events_handled.most_common(3)
                await ctx.channel.messages.send(", ".join("{}: {}".format(*x) for x in ev)
        
        """

        c = collections.Counter()
        for gw in self._gateways.values():
            c.update(gw._dispatches_handled)

        return c

    @property
    def gateways(self) -> 'typing.Mapping[int, GatewayHandler]':
        """
        :return: A read-only view of the current gateways for this client. 
        """
        return MappingProxyType(self._gateways)

    def find_channel(self,
                     channel_id: int) -> 'Union[None, dt_channel.Channel]':
        """
        Finds a channel by channel ID.
        """
        return self.state.find_channel(channel_id)

    async def get_gateway_url(self, get_shard_count: bool = True) \
            -> typing.Union[str, typing.Tuple[str, int]]:
        """
        :return: The gateway URL for this bot.
        """
        if get_shard_count:
            return await self.http.get_shard_count()
        else:
            return await self.http.get_gateway_url()

    def guilds_for(self, shard_id: int) -> 'typing.Iterable[dt_guild.Guild]':
        """
        Gets the guilds for this shard.

        :param shard_id: The shard ID to get guilds from.
        :return: A list of :class:`Guild` that client can see on the specified shard.
        """
        return self.state.guilds_for_shard(shard_id)

    def event(self, name: str):
        """
        A convenience decorator to mark a function as an event.

        This will copy it to the events dictionary, where it will be used as an event later on.

        .. code-block:: python3
        
            @bot.event("message_create")
            async def something(ctx, message: Message):
                pass

        :param name: The name of the event.
        """
        def _inner(func):
            f = ev_dec(name)(func)
            self.events.add_event(func=f)
            return func

        return _inner

    # rip in peace old fire_event
    # 2016-2017
    # broke my pycharm
    async def fire_event(self, event_name: str, *args, **kwargs):
        """
        Fires an event.

        This actually passes the arguments to :meth:`.EventManager.fire_event`.
        """
        gateway = kwargs.get("gateway")
        if not self.state.is_ready(gateway.gw_state.shard_id):
            if event_name not in self.IGNORE_READY and not event_name.startswith(
                    "gateway_"):
                return

        return await self.events.fire_event(event_name,
                                            *args,
                                            **kwargs,
                                            client=self)

    async def wait_for(self, *args, **kwargs) -> typing.Any:
        """
        Shortcut for :meth:`.EventManager.wait_for`.
        """
        return await self.events.wait_for(*args, **kwargs)

    # Gateway functions
    async def change_status(self,
                            game: Game = None,
                            status: Status = Status.ONLINE,
                            afk: bool = False,
                            shard_id: int = 0):
        """
        Changes the bot's current status.

        :param game: The game object to use. None for no game.
        :param status: The new status. Must be a :class:`.Status` object.
        :param afk: Is the bot AFK? Only useful for userbots.
        :param shard_id: The shard to change your status on.
        """

        gateway = self._gateways[shard_id]
        return await gateway.send_status(name=game.name if game else None,
                                         type_=game.type if game else None,
                                         url=game.url if game else None,
                                         status=status.value,
                                         afk=afk)

    # HTTP Functions
    async def edit_profile(self,
                           *,
                           username: str = None,
                           avatar: bytes = None):
        """
        Edits the profile of this bot.

        The user is **not** edited in-place - instead, you must wait for the `USER_UPDATE` event to
        be fired on the websocket.

        :param username: The new username of the bot.
        :param avatar: The bytes-like object that represents the new avatar you wish to use.
        """
        if username:
            if any(x in username for x in ('@', ':', '```')):
                raise ValueError("Username must not contain banned characters")

            if username in ("discordtag", "everyone", "here"):
                raise ValueError("Username cannot be a banned username")

            if not 2 <= len(username) <= 32:
                raise ValueError("Username must be 2-32 characters")

        if avatar:
            avatar = base64ify(avatar)

        await self.http.edit_user(username, avatar)

    async def edit_avatar(self, path: str):
        """
        A higher-level way to change your avatar.
        This allows you to provide a path to the avatar file instead of having to read it in 
        manually.

        :param path: The path-like object to the avatar file.
        """
        with open(path, 'rb') as f:
            return await self.edit_profile(avatar=f.read())

    async def get_user(self, user_id: int) -> User:
        """
        Gets a user by ID.

        :param user_id: The ID of the user to get.
        :return: A new :class:`.User` object.
        """
        try:
            return self.state._users[user_id]
        except KeyError:
            u = self.state.make_user(await self.http.get_user(user_id))
            # decache it if we need to
            self.state._check_decache_user(u.id)
            return u

    async def get_application(self, application_id: int) -> AppInfo:
        """
        Gets an application by ID.

        :param application_id: The client ID of the application to fetch.
        :return: A new :class:`.AppInfo` object corresponding to the application.
        """
        data = await self.http.get_app_info(application_id=application_id)
        appinfo = AppInfo(self, **data)

        return appinfo

    async def get_webhook(self, webhook_id: int) -> Webhook:
        """
        Gets a webhook by ID.

        :param webhook_id: The ID of the webhook to get.
        :return: A new :class:`.Webhook` object.
        """
        return self.state.make_webhook(await self.http.get_webhook(webhook_id))

    async def get_invite(self,
                         invite_code: str,
                         *,
                         with_counts: bool = True) -> Invite:
        """
        Gets an invite by code.

        :param invite_code: The invite code to get.
        :param with_counts: Return the approximate counts for this invite?
        :return: A new :class:`.Invite` object.
        """
        return Invite(
            self,
            **(await self.http.get_invite(invite_code,
                                          with_counts=with_counts)))

    async def get_widget(self, guild_id: int) -> Widget:
        """
        Gets a widget from a guild.
        
        :param guild_id: The ID of the guild to get the widget of. 
        :return: A :class:`.Widget` object.
        """
        data = await self.http.get_widget_data(guild_id)
        return Widget(self, **data)

    async def clean_content(self, content: str) -> str:
        """
        Cleans the content of a message, using the bot's cache.

        :param content: The content to clean.
        :return: The cleaned up message.
        """
        final = []
        tokens = content.split(" ")
        # o(2n) loop
        for token in tokens:
            # try and find a channel from public channels
            channel_match = CHANNEL_REGEX.match(token)
            if channel_match is not None:
                channel_id = int(channel_match.groups()[0])
                channel = self.state.find_channel(channel_id)
                if channel is None or channel.type not in \
                        [dt_channel.ChannelType.TEXT, dt_channel.ChannelType.VOICE]:
                    final.append("#deleted-channel")
                else:
                    final.append(f"#{channel.name}")

                continue

            user_match = MENTION_REGEX.match(token)
            if user_match is not None:
                found_name = None
                user_id = int(user_match.groups()[0])
                member_or_user = self.state.find_member_or_user(user_id)
                if member_or_user:
                    found_name = member_or_user.name

                if found_name is None:
                    final.append(token)
                else:
                    final.append(f"@{found_name}")

                continue

            emoji_match = EMOJI_REGEX.match(token)
            if emoji_match is not None:
                final.append(f":{emoji_match.groups()[0]}:")
                continue

            # if we got here, matching failed
            # so just add the token
            final.append(token)

        return " ".join(final)

    # download_ methods
    async def download_guild_member(self, guild_id: int,
                                    member_id: int) -> 'dt_member.Member':
        """
        Downloads a :class:`.Member` over HTTP.
        
        .. warning::
            
            The :attr:`.Member.roles` and similar fields will be empty when downloading a Member,
            unless the guild was in cache.
        
        :param guild_id: The ID of the guild which the member is in. 
        :param member_id: The ID of the member to get.
        :return: The :class:`.Member` object downloaded.
        """
        member_data = await self.http.get_guild_member(guild_id=guild_id,
                                                       member_id=member_id)
        member = dt_member.Member(self, **member_data)
        # this is enough to pick up the cache
        member.guild_id = guild_id

        # manual refcounts :ancap:
        self.state._check_decache_user(member.id)

        return member

    async def download_guild_members(
            self,
            guild_id: int,
            *,
            after: int = None,
            limit: int = 1000,
            get_all: bool = True) -> 'typing.Iterable[dt_member.Member]':
        """
        Downloads the members for a :class:`.Guild` over HTTP.
        
        .. warning::
        
            This can take a long time on big guilds.
        
        :param guild_id: The ID of the guild to download members for.
        :param after: The member ID after which to get members for.
        :param limit: The maximum number of members to return. By default, this is 1000 members.
        :param get_all: Should *all* members be fetched?
        :return: An iterable of :class:`.Member`.
        """
        member_data = []
        if get_all is True:
            last_id = 0
            while True:
                next_data = await self.http.get_guild_members(
                    guild_id=guild_id, limit=limit, after=last_id)
                # no more members to get
                if not next_data:
                    break

                member_data.extend(next_data)
                # if there's less data than limit, we are finished downloading members
                if len(next_data) < limit:
                    break

                last_id = member_data[-1]["user"]["id"]
        else:
            next_data = await self.http.get_guild_members(guild_id=guild_id,
                                                          limit=limit,
                                                          after=after)
            member_data.extend(next_data)

        # create the member objects
        members = []
        for datum in member_data:
            m = dt_member.Member(self, **datum)
            m.guild_id = guild_id
            members.append(m)

        return members

    async def download_channels(
            self, guild_id: int) -> 'typing.List[dt_channel.Channel]':
        """
        Downloads all the :class:`.Channel` for a Guild.
        
        :param guild_id: The ID of the guild to download channels for. 
        :return: An iterable of :class:`.Channel` objects.
        """
        channel_data = await self.http.get_guild_channels(guild_id=guild_id)
        channels = []
        for datum in channel_data:
            channel = dt_channel.Channel(self, **datum)
            channel.guild_id = guild_id
            channels.append(channel)

        return channels

    async def download_guild(self,
                             guild_id: int,
                             *,
                             full: bool = False) -> 'dt_guild.Guild':
        """
        Downloads a :class:`.Guild` over HTTP.
        
        .. warning::
        
            If ``full`` is True, this will fetch and fill ALL objects of the guild, including 
            channels and members. This can take a *long* time if the guild is large.
        
        :param guild_id: The ID of the Guild object to download. 
        :param full: If all extra data should be downloaded alongside it.
        :return: The :class:`.Guild` object downloaded.
        """
        guild_data = await self.http.get_guild(guild_id)
        # create the new guild using the data specified
        guild = dt_guild.Guild(self, **guild_data)
        guild.unavailable = False
        guild.from_guild_create(**guild_data)

        # update the guild store
        self.state._guilds[guild_id] = guild

        if full:
            # download all of the members
            members = await self.download_guild_members(guild_id=guild_id,
                                                        get_all=True)
            # update the `_members` dict
            guild._members = {m.id: m for m in members}

            # download all of the channels
            channels = await self.download_channels(guild_id=guild_id)
            guild._channels = {c.id: c for c in channels}

        return guild

    @ev_dec(name="gateway_dispatch_received")
    async def handle_dispatches(self, ctx: EventContext, name: str,
                                dispatch: dict):
        """
        Handles dispatches for the client.
        """
        try:
            handle_name = name.lower()
            handler = getattr(self.state, f"handle_{handle_name}")
        except AttributeError:
            logger.warning(f"Got unknown dispatch {name}")
            return
        else:
            logger.debug(f"Processing event {name}")

        try:
            with allow_external_makes():
                result = handler(ctx.gateway, dispatch)

                if inspect.isawaitable(result):
                    results = [await result]
                elif inspect.isasyncgen(result):
                    async with multio.asynclib.finalize_agen(result) as gen:
                        results = [r async for r in gen]
                else:
                    results = [result]

            for item in results:
                if not isinstance(item, tuple):
                    await self.events.fire_event(item,
                                                 gateway=ctx.gateway,
                                                 client=self)
                else:
                    await self.events.fire_event(item[0],
                                                 *item[1:],
                                                 gateway=ctx.gateway,
                                                 client=self)

        except Exception:
            logger.exception(
                f"Error decoding event {name} with data {dispatch}!")
            await self.kill()
            raise

    @ev_dec(name="ready")
    async def handle_ready(self, ctx: 'EventContext'):
        """
        Handles a READY event, dispatching a ``shards_ready`` event when all shards are ready.
        """
        self._ready_state[ctx.shard_id] = True

        if not all(self._ready_state.values()):
            return

        await self.events.fire_event("shards_ready",
                                     gateway=self._gateways[ctx.shard_id],
                                     client=self)

    async def handle_shard(self, shard_id: int, shard_count: int):
        """
        Handles a shard.

        :param shard_id: The shard ID to boot and handle.
        :param shard_count: The shard count to send in the identify packet.
        """
        # consume events
        async with open_websocket(self._token,
                                  self._gw_url,
                                  shard_id=shard_id,
                                  shard_count=shard_count) as gw:
            self._gateways[shard_id] = gw

            try:
                async with multio.asynclib.finalize_agen(gw.events()) as agen:
                    async for event in agen:
                        await self.fire_event(event[0], *event[1:], gateway=gw)
            except Exception as e:  # kill the bot if we failed to parse something
                await self.kill()
                raise
            finally:
                self._gateways.pop(shard_id, None)

    async def start(self, shard_count: int):
        """
        Starts the bot.

        :param shard_count: The number of shards to boot.
        """
        if self.bot_type & BotType.BOT:
            self.application_info = AppInfo(
                self, **(await self.http.get_app_info(None)))

        # update ready state
        for shard_id in range(shard_count):
            self._ready_state[shard_id] = False

        async with multio.asynclib.task_manager() as tg:
            self.task_manager = tg
            self.events.task_manager = tg

            for shard_id in range(0, shard_count):
                await multio.asynclib.spawn(tg, self.handle_shard, shard_id,
                                            shard_count)

    async def run_async(self, *, shard_count: int = 1, autoshard: bool = True):
        """
        Runs the client asynchronously.

        :param shard_count: The number of shards to boot.
        :param autoshard: If the bot should be autosharded.
        """
        if autoshard:
            url, shard_count = await self.get_gateway_url(get_shard_count=True)
        else:
            url, shard_count = await self.get_gateway_url(get_shard_count=False
                                                          ), shard_count

        self._gw_url = url
        self.shard_count = shard_count
        return await self.start(shard_count)

    async def kill(self) -> None:
        """
        Kills the bot by closing all shards.
        """
        for gateway in self._gateways.copy().values():
            await gateway.close(code=1006,
                                reason="Bot killed",
                                reconnect=False)

    def run(self, *, shard_count: int = 1, autoshard: bool = True, **kwargs):
        """
        Convenience method to run the bot with multio.

        :param shard_count: The number of shards to use. Ignored if autoshard is True.
        :param autoshard: If the bot should be autosharded.
        """

        p = functools.partial(self.run_async,
                              shard_count=shard_count,
                              autoshard=autoshard)
        multio.run(p, **kwargs)

    @classmethod
    def from_token(cls, token: str = None):
        """
        Starts a bot from a token object.

        :param token: The token to use for the bot.
        """
        bot = cls(token)
        return bot.run()
Exemple #4
0
class Client(object):
    """
    The main client class. This is used to interact with Discord.

    To start, you can create an instance of the client by passing it the token you want to use:

    .. code-block:: python3

        cl = Client("my.token.string")

    Registering events can be done with the :meth:`.Client.event` decorator, or alternatively
    manual usage of the :class:`.EventHandler` on :attr:`.Client.events`.

    .. code-block:: python3

        @cl.event("ready")
        async def loaded(ctx: EventContext):
            print("Bot logged in.")

    """
    #: A list of events to ignore the READY status.
    IGNORE_READY = [
        "connect", "guild_streamed", "guild_chunk", "guild_available",
        "guild_sync"
    ]

    def __init__(self,
                 token: str,
                 *,
                 state_klass: type = None,
                 bot_type: int = (BotType.BOT | BotType.ONLY_USER)):
        """
        :param token: The current token for this bot.
        :param state_klass: The class to construct the connection state from.
        :param bot_type: A union of :class:`~.BotType` that defines the type of this bot.
        """
        #: The mapping of `shard_id -> gateway` objects.
        self._gateways = {}

        #: The number of shards this client has.
        self.shard_count = 0

        #: The token for the bot.
        self._token = token

        if state_klass is None:
            from curious.core.state import State
            state_klass = State

        #: The current connection state for the bot.
        self.state = state_klass(self)

        #: The bot type for this bot.
        self.bot_type = bot_type

        if self.bot_type & BotType.BOT and self.bot_type & BotType.USERBOT:
            raise ValueError(
                "Bot cannot be a bot and a userbot at the same time")

        #: The current :class:`.EventManager` for this bot.
        self.events = EventManager()
        self.events.add_event(self.handle_dispatches,
                              name="gateway_dispatch_received")

        #: The :class:`~.HTTPClient` used for this bot.
        self.http = HTTPClient(self._token,
                               bot=bool(self.bot_type & BotType.BOT))

        #: The cached gateway URL.
        self._gw_url = None  # type: str

        #: The application info for this bot. Instance of :class:`~.AppInfo`.
        #: This will be None for user bots.
        self.application_info = None  # type: AppInfo

        self.scan_events()

    @property
    def user(self) -> BotUser:
        """
        :return: The :class:`~.User` that this client is logged in as.
        """
        return self.state._user

    @property
    def guilds(self) -> 'typing.Mapping[int, dt_guild.Guild]':
        """
        :return: A mapping of int -> :class:`~.Guild` that this client can see.
        """
        return self.state.guilds

    @property
    def invite_url(self):
        """
        :return: The invite URL for this bot.
        """
        return "https://discordapp.com/oauth2/authorize?client_id={}&scope=bot".format(
            self.application_info.client_id)

    @property
    def events_handled(self) -> collections.Counter:
        """
        A :class:`collections.Counter` of all events that have been handled since the bot's bootup.
        This can be used to track statistics for events.
         
        .. code-block:: python3
        
            @command()
            async def events(self, ctx: Context):
                '''
                Shows the most common events.
                '''
                
                ev = ctx.bot.events_handled.most_common(3)
                await ctx.channel.send(", ".join("{}: {}".format(*x) for x in ev)
        
        """

        c = collections.Counter()
        for gw in self._gateways.values():
            c.update(gw._dispatches_handled)

        return c

    @property
    def gateways(self):
        """
        :return: A read-only view of the current gateways for this client. 
        """
        return MappingProxyType(self._gateways)

    def find_channel(self, channel_id: int):
        """
        Finds a channel by channel ID.
        """
        return self.state.find_channel(channel_id)

    async def get_gateway_url(self) -> str:
        """
        :return: The gateway URL for this bot.
        """
        if self._gw_url:
            return self._gw_url

        self._gw_url = await self.http.get_gateway_url()
        return self._gw_url

    async def get_shard_count(self) -> int:
        """
        :return: The shard count recommended for this bot.
        """
        gw, shards = await self.http.get_shard_count()
        self._gw_url = gw

        return shards

    def guilds_for(self, shard_id: int) -> 'typing.Iterable[dt_guild.Guild]':
        """
        Gets the guilds for this shard.

        :param shard_id: The shard ID to get guilds from.
        :return: A list of :class:`Guild` that client can see on the specified shard.
        """
        return self.state.guilds_for_shard(shard_id)

    def event(self, name: str):
        """
        A convenience decorator to mark a function as an event.

        This will copy it to the events dictionary, where it will be used as an event later on.

        .. code-block:: python3
        
            @bot.event("message_create")
            async def something(ctx, message: Message):
                pass

        :param name: The name of the event.
        """
        def _inner(func):
            f = ev_dec(name)(func)
            self.events.add_event(func=f)

        return _inner

    def scan_events(self):
        """
        Scans this class for functions marked with an event decorator.
        """
        def _pred(f):
            if not hasattr(f, "events"):
                return False

            if getattr(f, "scan", False):
                return True

            return False

        for _, item in inspect.getmembers(self, predicate=_pred):
            logger.info("Registering event function {} for events {}".format(
                _, item.events))
            self.events.add_event(item)

    # rip in peace old fire_event
    # 2016-2017
    # broke my pycharm
    async def fire_event(self, event_name: str, *args, **kwargs):
        """
        Fires an event.

        This actually passes the arguments to :meth:`.EventManager.fire_event`.
        """
        gateway = kwargs.get("gateway")
        if not self.state.is_ready(gateway.shard_id):
            if event_name not in self.IGNORE_READY and not event_name.startswith(
                    "gateway_"):
                return

        return await self.events.fire_event(event_name,
                                            *args,
                                            **kwargs,
                                            client=self)

    async def wait_for(self, *args, **kwargs) -> typing.Any:
        """
        Shortcut for :meth:`.EventManager.wait_for`.
        """
        return await self.events.wait_for(*args, **kwargs)

    # Gateway functions
    async def change_status(self,
                            game: Game = None,
                            status: Status = Status.ONLINE,
                            afk: bool = False,
                            shard_id: int = 0,
                            *,
                            sync: bool = False):
        """
        Changes the bot's current status.

        :param game: The game object to use. None for no game.
        :param status: The new status. Must be a :class:`~.Status` object.
        :param afk: Is the bot AFK? Only useful for userbots.
        :param shard_id: The shard to change your status on.
        :param sync: Sync status with other clients? Only useful for userbots.
        """
        if not self.user.bot and sync:
            # update `status` key of settings
            await self.user.settings.update(status=status.value)

        gateway = self._gateways[shard_id]
        return await gateway.send_status(game, status, afk=afk)

    # HTTP Functions
    async def edit_profile(self,
                           *,
                           username: str = None,
                           avatar: bytes = None,
                           password: str = None):
        """
        Edits the profile of this bot.

        The user is **not** edited in-place - instead, you must wait for the `USER_UPDATE` event to
        be fired on the websocket.

        :param username: The new username of the bot.
        :param avatar: The bytes-like object that represents the new avatar you wish to use.
        :param password: The password to use. Only for user accounts.
        """
        if not self.user.bot and password is None:
            raise ValueError("Password must be passed for user bots")

        if username:
            if any(x in username for x in ('@', ':', '```')):
                raise ValueError("Username must not contain banned characters")

            if username in ("discordtag", "everyone", "here"):
                raise ValueError("Username cannot be a banned username")

            if not 2 <= len(username) <= 32:
                raise ValueError("Username must be 2-32 characters")

        if avatar:
            avatar = base64ify(avatar)

        await self.http.edit_user(username, avatar, password)

    async def edit_avatar(self, path: str):
        """
        A higher-level way to change your avatar.
        This allows you to provide a path to the avatar file instead of having to read it in 
        manually.

        :param path: The path-like object to the avatar file.
        """
        with open(path, 'rb') as f:
            return await self.edit_profile(avatar=f.read())

    async def get_user(self, user_id: int) -> User:
        """
        Gets a user by ID.

        :param user_id: The ID of the user to get.
        :return: A new :class:`~.User` object.
        """
        try:
            return self.state._users[user_id]
        except KeyError:
            u = self.state.make_user(await self.http.get_user(user_id))
            # decache it if we need to
            self.state._check_decache_user(u.id)
            return u

    async def get_application(self, application_id: int) -> AppInfo:
        """
        Gets an application by ID.

        :param application_id: The client ID of the application to fetch.
        :return: A new :class:`~.AppInfo` object corresponding to the application.
        """
        data = await self.http.get_app_info(application_id=application_id)
        appinfo = AppInfo(self, **data)

        return appinfo

    async def get_webhook(self, webhook_id: int) -> Webhook:
        """
        Gets a webhook by ID.

        :param webhook_id: The ID of the webhook to get.
        :return: A new :class:`~.Webhook` object.
        """
        return self.state.make_webhook(await self.http.get_webhook(webhook_id))

    async def get_invite(self,
                         invite_code: str,
                         *,
                         with_counts: bool = True) -> Invite:
        """
        Gets an invite by code.

        :param invite_code: The invite code to get.
        :param with_counts: Return the approximate counts for this invite?
        :return: A new :class:`~.Invite` object.
        """
        return Invite(
            self,
            **(await self.http.get_invite(invite_code,
                                          with_counts=with_counts)))

    async def get_widget(self, guild_id: int) -> Widget:
        """
        Gets a widget from a guild.
        
        :param guild_id: The ID of the guild to get the widget of. 
        :return: A :class:`~.Widget` object.
        """
        data = await self.http.get_widget_data(guild_id)
        return Widget(self, **data)

    # download_ methods
    async def download_guild_member(self, guild_id: int,
                                    member_id: int) -> 'dt_member.Member':
        """
        Downloads a :class:`~.Member` over HTTP.
        
        .. warning::
            
            The :attr:`~.Member.roles` and similar fields will be empty when downloading a Member, 
            unless the guild was in cache.
        
        :param guild_id: The ID of the guild which the member is in. 
        :param member_id: The ID of the member to get.
        :return: The :class:`~.Member` object downloaded.
        """
        member_data = await self.http.get_guild_member(guild_id=guild_id,
                                                       member_id=member_id)
        member = dt_member.Member(self, **member_data)
        # this is enough to pick up the cache
        member.guild_id = guild_id

        # manual refcounts :ancap:
        self.state._check_decache_user(member.id)

        return member

    async def download_guild_members(
            self,
            guild_id: int,
            *,
            after: int = None,
            limit: int = 1000,
            get_all: bool = True) -> 'typing.Iterable[dt_member.Member]':
        """
        Downloads the members for a :class:`~.Guild` over HTTP.
        
        .. warning::
        
            This can take a long time on big guilds.
        
        :param guild_id: The ID of the guild to download members for.
        :param after: The member ID after which to get members for.
        :param limit: The maximum number of members to return. By default, this is 1000 members.
        :param get_all: Should *all* members be fetched?
        :return: An iterable of :class:`~.Member`.
        """
        member_data = []
        if get_all is True:
            last_id = 0
            while True:
                next_data = await self.http.get_guild_members(
                    guild_id=guild_id, limit=limit, after=last_id)
                # no more members to get
                if not next_data:
                    break

                member_data.extend(next_data)
                # if there's less data than limit, we are finished downloading members
                if len(next_data) < limit:
                    break

                last_id = member_data[-1]["user"]["id"]
        else:
            next_data = await self.http.get_guild_members(guild_id=guild_id,
                                                          limit=limit,
                                                          after=after)
            member_data.extend(next_data)

        # create the member objects
        members = []
        for datum in member_data:
            m = dt_member.Member(self, **datum)
            m.guild_id = guild_id
            members.append(m)

        return members

    async def download_channels(
            self, guild_id: int) -> 'typing.List[dt_channel.Channel]':
        """
        Downloads all the :class:`~.Channel` for a Guild.
        
        :param guild_id: The ID of the guild to download channels for. 
        :return: An iterable of :class:`~.Channel` objects.
        """
        channel_data = await self.http.get_guild_channels(guild_id=guild_id)
        channels = []
        for datum in channel_data:
            channel = dt_channel.Channel(self, **datum)
            channel.guild_id = guild_id
            channels.append(channel)

        return channels

    async def download_guild(self,
                             guild_id: int,
                             *,
                             full: bool = False) -> 'dt_guild.Guild':
        """
        Downloads a :class:`~.Guild` over HTTP.
        
        .. warning::
        
            If ``full`` is True, this will fetch and fill ALL objects of the guild, including 
            channels and members. This can take a *long* time if the guild is large.
        
        :param guild_id: The ID of the Guild object to download. 
        :param full: If all extra data should be downloaded alongside it.
        :return: The :class:`~.Guild` object downloaded.
        """
        guild_data = await self.http.get_guild(guild_id)
        # create the new guild using the data specified
        guild = dt_guild.Guild(self, **guild_data)
        guild.unavailable = False

        # update the guild store
        self.state._guilds[guild_id] = guild

        if full:
            # download all of the members
            members = await self.download_guild_members(guild_id=guild_id,
                                                        get_all=True)
            # update the `_members` dict
            guild._members = {m.id: m for m in members}

            # download all of the channels
            channels = await self.download_channels(guild_id=guild_id)
            guild._channels = {c.id: c for c in channels}

        return guild

    async def handle_dispatches(self, ctx: EventContext, name: str,
                                dispatch: dict):
        """
        Handles dispatches for the client.
        """
        try:
            handle_name = name.lower()
            handler = getattr(self.state, f"handle_{handle_name}")
        except AttributeError:
            logger.warning(f"Got unknown dispatch {name}")
            return
        else:
            logger.debug(f"Processing event {name}")

        try:
            result = handler(ctx.gateway, dispatch)

            if inspect.isawaitable(result):
                result = await result
            elif inspect.isasyncgen(result):
                async with multio.finalize_agen(result) as gen:
                    async for i in gen:
                        await self.events.fire_event(i[0],
                                                     *i[1:],
                                                     gateway=ctx.gateway,
                                                     client=self)

                # no more processing after the async gen
                return

            if not isinstance(result, tuple):
                await self.events.fire_event(result,
                                             gateway=ctx.gateway,
                                             client=self)
            else:
                await self.events.fire_event(result[0],
                                             *result[1:],
                                             gateway=ctx.gateway,
                                             client=self)

        # TODO: Change this from being an exception to being an event.
        except ChunkGuilds:
            ctx.gateway._get_chunks()
        except Exception:
            logger.exception(
                f"Error decoding event {event} with data {dispatch}!")
            await ctx.gateway.close(code=1006, reason="Internal client error")
            raise

    async def handle_shard(self, shard_id: int, shard_count: int):
        """
        Handles a shard.

        :param shard_id: The shard ID to boot and handle.
        :param shard_count: The shard count to send in the identify packet.
        """

        total_tries = 0
        while True:
            # keep retrying connecting
            if total_tries == 10:
                raise RuntimeError(f"Gave up reconnecting shard id {shard_id}")

            try:
                gw = await Gateway.from_token(self._token,
                                              self.state,
                                              await self.get_gateway_url(),
                                              shard_id=shard_id,
                                              shard_count=shard_count)
            except Exception:
                # give up
                logger.exception("Failed to connect to the gateway...")
                total_tries += 1
                continue
            else:
                # reset total tries, we made it
                total_tries = 0
                self._gateways[shard_id] = gw

            try:
                try:
                    # consume events
                    async with multio.finalize_agen(gw.events()) as agen:
                        async for event in agen:
                            await self.fire_event(event[0],
                                                  *event[1:],
                                                  gateway=gw)

                except WebsocketClosed as e:
                    # Try and handle the close.
                    if e.reason == "Client closed connection":
                        # internal
                        return

                    if e.code in [1000, 4007] or gw.session_id is None:
                        logger.info("Shard {} disconnected with code {}, "
                                    "creating new session".format(
                                        shard_id, e.code))

                        self.state._reset(gw.shard_id)
                        await gw.reconnect(resume=False)
                    elif e.code not in (4004, 4011):
                        # Try and RESUME.
                        logger.info(
                            "Shard {} disconnected with close code {}, reason {}, "
                            "attempting a reconnect.".format(
                                shard_id, e.code, e.reason))

                        await gw.reconnect(resume=True)
                    else:
                        raise
                except ReconnectWebsocket:
                    # We've been told to reconnect, try and RESUME.
                    await gw.reconnect(resume=True)
            except (WebsocketClosed, WebsocketUnusable):
                # give up and retry.
                total_tries += 1
                continue

    async def start(self, shard_count: int):
        """
        Starts the bot.

        :param shard_count: The number of shards to boot.
        """
        if self.bot_type & BotType.BOT:
            self.application_info = AppInfo(
                self, **(await self.http.get_app_info(None)))

        async with multio.asynclib.task_manager() as tg:
            self.events.task_manager = tg

            for shard_id in range(0, shard_count):
                await tg.spawn(self.handle_shard(shard_id, shard_count))

    async def run_async(self, *, shard_count: int = 1, autoshard: bool = True):
        """
        Runs the client asynchronously.

        :param shard_count: The number of shards to boot.
        :param autoshard: If the bot should be autosharded.
        """
        if autoshard:
            shard_count = await self.get_shard_count()

        self.shard_count = shard_count
        return await self.start(shard_count)

    def run(self, *, shard_count: int = 1, autoshard: bool = True):
        """
        Convenience method to run the bot with a multio handler.

        :param shard_count: The number of shards to use. Ignored if autoshard is True.
        :param autoshard: If the bot should be autosharded.
        """

        try:
            p = functools.partial(self.run_async,
                                  shard_count=shard_count,
                                  autoshard=autoshard)
            multio.run(p)
        except (KeyboardInterrupt, EOFError):
            pass

    @classmethod
    def from_token(cls, token: str = None):
        """
        Starts a bot from a token object.

        :param token: The token to use for the bot.
        """
        bot = cls(token)
        return bot.run()