def waitForEvents(self):
     """
     Pause and do not exit.  Wait over events from the server.
     This is optional. You should not use this method in ipython notebook
     Event will get called anyway.
     """
     WebSocketClient.run_forever(self)
Example #2
0
 def waitForEvents(self):
     """
     Pause and do not exit.  Wait over events from the server.
     This is optional. You should not use this method in ipython notebook
     Event will get called anyway.
     """
     WebSocketClient.run_forever(self)
Example #3
0
class Client(object):
    def __init__(self, listener):
        self.listener = listener
        self._ws_url = 'wss://ws.bitso.com'
        self.ws_client = WebSocketClient(self._ws_url)
        self.ws_client.opened = self.listener.on_connect
        self.ws_client.received_message = self._received
        self.ws_client.closed = self._closed
        self.channels = []

    def connect(self, channels):
        self.channels = channels
        self.ws_client.opened = self._opened
        self.ws_client.connect()
        self.ws_client.run_forever()

    def close(self):
        self.ws_client.close()
        
    def _opened(self):
        for channel in self.channels:
            self.ws_client.send(json.dumps({ 'action': 'subscribe', 'book': 'btc_mxn', 'type': channel }))
        thread.start_new_thread(self._ping_server, ())
        self.listener.on_connect()

    def _received(self, m):
        #print m.data
        val = json.loads(m.data)
        obj = StreamUpdate(val)
        self.listener.on_update(obj)
        
    def _closed(self, code, reason):
        self.listener.on_close(code, reason)
        
    def _ping_server(self):
        while True:
            #keep-alive
            self.ws_client.send(PingControlMessage(u'ping'))
            time.sleep(20)
Example #4
0
class Client(object):
    """Represents a client connection that connects to Discord.
    This class is used to interact with the Discord WebSocket and API.

     .. attribute:: user

         A :class:`User` that represents the connected client. None if not logged in.
     .. attribute:: servers

         A list of :class:`Server` that the connected client has available.
     .. attribute:: private_channels

         A list of :class:`PrivateChannel` that the connected client is participating on.
    """
    def __init__(self, **kwargs):
        self._is_logged_in = False
        self.user = None
        self.servers = []
        self.private_channels = []
        self.token = ''
        self.events = {
            'on_ready': _null_event,
            'on_disconnect': _null_event,
            'on_error': _null_event,
            'on_response': _null_event,
            'on_message': _null_event
        }

        self.ws = WebSocketClient(endpoints.WEBSOCKET_HUB,
                                  protocols=['http-only', 'chat'])

        # this is kind of hacky, but it's to avoid deadlocks.
        # i.e. python does not allow me to have the current thread running if it's self
        # it throws a 'cannot join current thread' RuntimeError
        # So instead of doing a basic inheritance scheme, we're overriding the member functions.

        self.ws.opened = self._opened
        self.ws.closed = self._closed
        self.ws.received_message = self._received_message
        self.ws.connect()

        # the actual headers for the request...
        # we only override 'authorization' since the rest could use the defaults.
        self.headers = {
            'authorization': self.token,
        }

    def _received_message(self, msg):
        response = json.loads(str(msg))
        if response.get('op') != 0:
            return

        self.events['on_response'](response)
        event = response.get('t')
        data = response.get('d')

        if event == 'READY':
            self.user = User(**data['user'])
            guilds = data.get('guilds')

            for guild in guilds:
                guild['roles'] = [role.get('name') for role in guild['roles']]
                guild['members'] = [
                    User(**member['user']) for member in guild['members']
                ]

                self.servers.append(Server(**guild))
                channels = [
                    Channel(server=self.servers[-1], **channel)
                    for channel in guild['channels']
                ]
                self.servers[-1].channels = channels

            for pm in data.get('private_channels'):
                self.private_channels.append(
                    PrivateChannel(id=pm['id'], user=User(**pm['recipient'])))

            # set the keep alive interval..
            self.ws.heartbeat_freq = data.get('heartbeat_interval')

            # we're all ready
            self.events['on_ready']()
        elif event == 'MESSAGE_CREATE':
            channel = self.get_channel(data.get('channel_id'))
            message = Message(channel=channel, **data)
            self.events['on_message'](message)

    def _opened(self):
        print('Opened!')

    def _closed(self, code, reason=None):
        print('closed with ', code, reason)

    def run(self):
        """Runs the client and allows it to receive messages and events."""
        self.ws.run_forever()

    @property
    def is_logged_in(self):
        """Returns True if the client is successfully logged in. False otherwise."""
        return self._is_logged_in

    def get_channel(self, id):
        """Returns a :class:`Channel` or :class:`PrivateChannel` with the following ID. If not found, returns None."""
        if id is None:
            return None

        for server in self.servers:
            for channel in server.channels:
                if channel.id == id:
                    return channel

        for pm in self.private_channels:
            if pm.id == id:
                return pm

    def start_private_message(self, user):
        """Starts a private message with the user. This allows you to :meth:`send_message` to it.

        Note that this method should rarely be called as :meth:`send_message` does it automatically.

        :param user: A :class:`User` to start the private message with.
        """
        if not isinstance(user, User):
            raise TypeError('user argument must be a User')

        payload = {'recipient_id': user.id}

        r = response.post('{}/{}/channels'.format(endpoints.USERS,
                                                  self.user.id),
                          json=payload,
                          headers=self.headers)
        if r.status_code == 200:
            data = r.json()
            self.private_channels.append(
                PrivateChannel(id=data['id'], user=user))

    def send_message(self, destination, content, mentions=True):
        """Sends a message to the destination given with the content given.

        The destination could be a :class:`Channel` or a :class:`PrivateChannel`. For convenience
        it could also be a :class:`User`. If it's a :class:`User` or :class:`PrivateChannel` then it
        sends the message via private message, otherwise it sends the message to the channel.

        The content must be a type that can convert to a string through ``str(content)``.

        The mentions must be either an array of :class:`User` to mention or a boolean. If
        ``mentions`` is ``True`` then all the users mentioned in the content are mentioned, otherwise
        no one is mentioned. Note that to mention someone in the content, you should use :meth:`User.mention`.

        :param destination: The location to send the message.
        :param content: The content of the message to send.
        :param mentions: A list of :class:`User` to mention in the message or a boolean. Ignored for private messages.
        """

        channel_id = ''
        is_private_message = True
        if isinstance(destination, Channel) or isinstance(
                destination, PrivateChannel):
            channel_id = destination.id
            is_private_message = destination.is_private
        elif isinstance(destination, User):
            found = next(
                (pm for pm in self.private_channels if pm.user == destination),
                None)
            if found is None:
                # Couldn't find the user, so start a PM with them first.
                self.start_private_message(destination)
                channel_id = self.private_channels[-1].id
            else:
                channel_id = found.id
        else:
            raise InvalidDestination(
                'Destination must be Channel, PrivateChannel, or User')

        content = str(content)

        if isinstance(mentions, list):
            mentions = [user.id for user in mentions]
        elif mentions == True:
            mentions = re.findall(r'@<(\d+)>', content)
        else:
            mentions = []

        url = '{base}/{id}/messages'.format(base=endpoints.CHANNELS,
                                            id=channel_id)
        payload = {
            'content': content,
        }

        if not is_private_message:
            payload['mentions'] = mentions

        response = requests.post(url, json=payload, headers=self.headers)

    def login(self, email, password):
        """Logs in the user with the following credentials.

        After this function is called, :attr:`is_logged_in` returns True if no
        errors occur.

        :param str email: The email used to login.
        :param str password: The password used to login.
        """

        payload = {'email': email, 'password': password}

        r = requests.post(endpoints.LOGIN, json=payload)

        if r.status_code == 200:
            body = r.json()
            self.token = body['token']
            self.headers['authorization'] = self.token
            second_payload = {
                'op': 2,
                'd': {
                    'token': self.token,
                    'properties': {
                        '$os': sys_platform,
                        '$browser': 'pydiscord',
                        '$device': 'pydiscord',
                        '$referrer': '',
                        '$referring_domain': ''
                    }
                }
            }

            self.ws.send(json.dumps(second_payload))
            self._is_logged_in = True

    def event(self, function):
        """A decorator that registers an event to listen to.

        You can find more info about the events on the :ref:`documentation below <discord-api-events>`.

        Example: ::

            @client.event
            def on_ready():
                print('Ready!')
        """

        if function.__name__ not in self.events:
            raise InvalidEventName(
                'The function name {} is not a valid event name'.format(
                    function.__name__))

        self.events[function.__name__] = function
        return function
Example #5
0
class Client(object):
    """Represents a client connection that connects to Discord.
    This class is used to interact with the Discord WebSocket and API.

    A number of options can be passed to the :class:`Client` via keyword arguments.

    :param int max_length: The maximum number of messages to store in :attr:`messages`. Defaults to 5000.

    Instance attributes:

     .. attribute:: user

         A :class:`User` that represents the connected client. None if not logged in.
     .. attribute:: servers

         A list of :class:`Server` that the connected client has available.
     .. attribute:: private_channels

         A list of :class:`PrivateChannel` that the connected client is participating on.
     .. attribute:: messages

        A deque_ of :class:`Message` that the client has received from all servers and private messages.

    .. _deque: https://docs.python.org/3.4/library/collections.html#collections.deque
    """

    def __init__(self, **kwargs):
        self._is_logged_in = False
        self.user = None
        self.servers = []
        self.private_channels = []
        self.token = ''
        self.messages = deque([], maxlen=kwargs.get('max_length', 5000))
        self.events = {
            'on_ready': _null_event,
            'on_disconnect': _null_event,
            'on_error': _null_event,
            'on_response': _null_event,
            'on_message': _null_event,
            'on_message_delete': _null_event
        }

        self.ws = WebSocketClient(endpoints.WEBSOCKET_HUB, protocols=['http-only', 'chat'])

        # this is kind of hacky, but it's to avoid deadlocks.
        # i.e. python does not allow me to have the current thread running if it's self
        # it throws a 'cannot join current thread' RuntimeError
        # So instead of doing a basic inheritance scheme, we're overriding the member functions.

        self.ws.opened = self._opened
        self.ws.closed = self._closed
        self.ws.received_message = self._received_message
        self.ws.connect()

        # the actual headers for the request...
        # we only override 'authorization' since the rest could use the defaults.
        self.headers = {
            'authorization': self.token,
        }

    def _received_message(self, msg):
        response = json.loads(str(msg))
        if response.get('op') != 0:
            return

        self.events['on_response'](response)
        event = response.get('t')
        data = response.get('d')

        if event == 'READY':
            self.user = User(**data['user'])
            guilds = data.get('guilds')

            for guild in guilds:
                guild['roles'] = [role.get('name') for role in guild['roles']]
                guild['members'] = [User(**member['user']) for member in guild['members']]

                self.servers.append(Server(**guild))
                channels = [Channel(server=self.servers[-1], **channel) for channel in guild['channels']]
                self.servers[-1].channels = channels

            for pm in data.get('private_channels'):
                self.private_channels.append(PrivateChannel(id=pm['id'], user=User(**pm['recipient'])))

            # set the keep alive interval..
            self.ws.heartbeat_freq = data.get('heartbeat_interval')

            # we're all ready
            self.events['on_ready']()
        elif event == 'MESSAGE_CREATE':
            channel = self.get_channel(data.get('channel_id'))
            message = Message(channel=channel, **data)
            self.events['on_message'](message)
            self.messages.append(message)
        elif event == 'MESSAGE_DELETE':
            channel = self.get_channel(data.get('channel_id'))
            message_id = data.get('id')
            found = next((m for m in self.messages if m.id == message_id), None)
            if found is not None:
                self.events['on_message_delete'](found)
                self.messages.remove(found)

    def _opened(self):
        print('Opened!')

    def _closed(self, code, reason=None):
        print('closed with ', code, reason)

    def run(self):
        """Runs the client and allows it to receive messages and events."""
        self.ws.run_forever()

    @property
    def is_logged_in(self):
        """Returns True if the client is successfully logged in. False otherwise."""
        return self._is_logged_in

    def get_channel(self, id):
        """Returns a :class:`Channel` or :class:`PrivateChannel` with the following ID. If not found, returns None."""
        if id is None:
            return None

        for server in self.servers:
            for channel in server.channels:
                if channel.id == id:
                    return channel

        for pm in self.private_channels:
            if pm.id == id:
                return pm

    def start_private_message(self, user):
        """Starts a private message with the user. This allows you to :meth:`send_message` to it.

        Note that this method should rarely be called as :meth:`send_message` does it automatically.

        :param user: A :class:`User` to start the private message with.
        """
        if not isinstance(user, User):
            raise TypeError('user argument must be a User')

        payload = {
            'recipient_id': user.id
        }

        r = response.post('{}/{}/channels'.format(endpoints.USERS, self.user.id), json=payload, headers=self.headers)
        if r.status_code == 200:
            data = r.json()
            self.private_channels.append(PrivateChannel(id=data['id'], user=user))

    def send_message(self, destination, content, mentions=True):
        """Sends a message to the destination given with the content given.

        The destination could be a :class:`Channel` or a :class:`PrivateChannel`. For convenience
        it could also be a :class:`User`. If it's a :class:`User` or :class:`PrivateChannel` then it
        sends the message via private message, otherwise it sends the message to the channel.

        The content must be a type that can convert to a string through ``str(content)``.

        The mentions must be either an array of :class:`User` to mention or a boolean. If
        ``mentions`` is ``True`` then all the users mentioned in the content are mentioned, otherwise
        no one is mentioned. Note that to mention someone in the content, you should use :meth:`User.mention`.

        :param destination: The location to send the message.
        :param content: The content of the message to send.
        :param mentions: A list of :class:`User` to mention in the message or a boolean. Ignored for private messages.
        """

        channel_id = ''
        is_private_message = True
        if isinstance(destination, Channel) or isinstance(destination, PrivateChannel):
            channel_id = destination.id
            is_private_message = destination.is_private
        elif isinstance(destination, User):
            found = next((pm for pm in self.private_channels if pm.user == destination), None)
            if found is None:
                # Couldn't find the user, so start a PM with them first.
                self.start_private_message(destination)
                channel_id = self.private_channels[-1].id
            else:
                channel_id = found.id
        else:
            raise InvalidDestination('Destination must be Channel, PrivateChannel, or User')

        content = str(content)

        if isinstance(mentions, list):
            mentions = [user.id for user in mentions]
        elif mentions == True:
            mentions = re.findall(r'@<(\d+)>', content)
        else:
            mentions = []

        url = '{base}/{id}/messages'.format(base=endpoints.CHANNELS, id=channel_id)
        payload = {
            'content': content,
        }

        if not is_private_message:
            payload['mentions'] = mentions

        response = requests.post(url, json=payload, headers=self.headers)


    def login(self, email, password):
        """Logs in the user with the following credentials.

        After this function is called, :attr:`is_logged_in` returns True if no
        errors occur.

        :param str email: The email used to login.
        :param str password: The password used to login.
        """

        payload = {
            'email': email,
            'password': password
        }

        r = requests.post(endpoints.LOGIN, json=payload)

        if r.status_code == 200:
            body = r.json()
            self.token = body['token']
            self.headers['authorization'] = self.token
            second_payload = {
                'op': 2,
                'd': {
                    'token': self.token,
                    'properties': {
                        '$os': sys_platform,
                        '$browser': 'pydiscord',
                        '$device': 'pydiscord',
                        '$referrer': '',
                        '$referring_domain': ''
                    }
                }
            }

            self.ws.send(json.dumps(second_payload))
            self._is_logged_in = True

    def event(self, function):
        """A decorator that registers an event to listen to.

        You can find more info about the events on the :ref:`documentation below <discord-api-events>`.

        Example: ::

            @client.event
            def on_ready():
                print('Ready!')
        """

        if function.__name__ not in self.events:
            raise InvalidEventName('The function name {} is not a valid event name'.format(function.__name__))

        self.events[function.__name__] = function
        return function
Example #6
0
class Client(object):
    """Represents a client connection that connects to Discord.
    This class is used to interact with the Discord WebSocket and API.

    A number of options can be passed to the :class:`Client` via keyword arguments.

    :param int max_length: The maximum number of messages to store in :attr:`messages`. Defaults to 5000.

    Instance attributes:

     .. attribute:: user

         A :class:`User` that represents the connected client. None if not logged in.
     .. attribute:: servers

         A list of :class:`Server` that the connected client has available.
     .. attribute:: private_channels

         A list of :class:`PrivateChannel` that the connected client is participating on.
     .. attribute:: messages

        A deque_ of :class:`Message` that the client has received from all servers and private messages.
     .. attribute:: email

        The email used to login. This is only set if login is successful, otherwise it's None.

    .. _deque: https://docs.python.org/3.4/library/collections.html#collections.deque
    """

    def __init__(self, **kwargs):
        self._is_logged_in = False
        self.user = None
        self.email = None
        self.servers = []
        self.private_channels = []
        self.token = ''
        self.messages = deque([], maxlen=kwargs.get('max_length', 5000))
        self.events = {
            'on_ready': _null_event,
            'on_disconnect': _null_event,
            'on_error': _null_event,
            'on_response': _null_event,
            'on_message': _null_event,
            'on_message_delete': _null_event,
            'on_message_edit': _null_event,
            'on_status': _null_event,
            'on_channel_delete': _null_event,
            'on_channel_create': _null_event,
            'on_channel_update': _null_event,
            'on_member_join': _null_event,
            'on_member_remove': _null_event,
            'on_member_update': _null_event,
            'on_server_create': _null_event,
            'on_server_delete': _null_event,
        }

        # the actual headers for the request...
        # we only override 'authorization' since the rest could use the defaults.
        self.headers = {
            'authorization': self.token,
        }

    def _get_message(self, msg_id):
        return utils.find(lambda m: m.id == msg_id, self.messages)

    def _get_server(self, guild_id):
        return utils.find(lambda g: g.id == guild_id, self.servers)

    def _add_server(self, guild):
        guild['roles'] = [Role(**role) for role in guild['roles']]
        members = guild['members']
        owner = guild['owner_id']
        for i, member in enumerate(members):
            roles = member['roles']
            for j, roleid in enumerate(roles):
                role = utils.find(lambda r: r.id == roleid, guild['roles'])
                if role is not None:
                    roles[j] = role
            members[i] = Member(**member)

            # found the member that owns the server
            if members[i].id == owner:
                owner = members[i]

        for presence in guild['presences']:
            user_id = presence['user']['id']
            member = utils.find(lambda m: m.id == user_id, members)
            if member is not None:
                member.status = presence['status']
                member.game_id = presence['game_id']


        server = Server(owner=owner, **guild)

        # give all the members their proper server
        for member in server.members:
            member.server = server

        channels = [Channel(server=server, **channel) for channel in guild['channels']]
        server.channels = channels
        self.servers.append(server)

    def _create_websocket(self, url, reconnect=False):
        if url is None:
            raise GatewayNotFound()
        log.info('websocket gateway found')
        self.ws = WebSocketClient(url, protocols=['http-only', 'chat'])

        # this is kind of hacky, but it's to avoid deadlocks.
        # i.e. python does not allow me to have the current thread running if it's self
        # it throws a 'cannot join current thread' RuntimeError
        # So instead of doing a basic inheritance scheme, we're overriding the member functions.

        self.ws.opened = self._opened
        self.ws.closed = self._closed
        self.ws.received_message = self._received_message
        self.ws.connect()
        log.info('websocket has connected')

        if reconnect == False:
            second_payload = {
                'op': 2,
                'd': {
                    'token': self.token,
                    'properties': {
                        '$os': sys.platform,
                        '$browser': 'discord.py',
                        '$device': 'discord.py',
                        '$referrer': '',
                        '$referring_domain': ''
                    },
                    'v': 2
                }
            }

            self.ws.send(json.dumps(second_payload))

    def _resolve_mentions(self, content, mentions):
        if isinstance(mentions, list):
            return [user.id for user in mentions]
        elif mentions == True:
            return re.findall(r'<@(\d+)>', content)
        else:
            return []

    def _invoke_event(self, event_name, *args, **kwargs):
        try:
            log.info('attempting to invoke event {}'.format(event_name))
            self.events[event_name](*args, **kwargs)
        except Exception as e:
            log.error('an error ({}) occurred in event {} so on_error is invoked instead'.format(type(e).__name__, event_name))
            self.events['on_error'](event_name, *sys.exc_info())

    def _received_message(self, msg):
        response = json.loads(str(msg))
        log.debug('WebSocket Event: {}'.format(response))
        if response.get('op') != 0:
            return

        self._invoke_event('on_response', response)
        event = response.get('t')
        data = response.get('d')

        if event == 'READY':
            self.user = User(**data['user'])
            guilds = data.get('guilds')

            for guild in guilds:
                self._add_server(guild)

            for pm in data.get('private_channels'):
                self.private_channels.append(PrivateChannel(id=pm['id'], user=User(**pm['recipient'])))

            # set the keep alive interval..
            interval = data.get('heartbeat_interval') / 1000.0
            self.keep_alive = KeepAliveHandler(interval, self.ws)
            self.keep_alive.start()

            # we're all ready
            self._invoke_event('on_ready')
        elif event == 'MESSAGE_CREATE':
            channel = self.get_channel(data.get('channel_id'))
            message = Message(channel=channel, **data)
            self._invoke_event('on_message', message)
            self.messages.append(message)
        elif event == 'MESSAGE_DELETE':
            channel = self.get_channel(data.get('channel_id'))
            message_id = data.get('id')
            found = self._get_message(message_id)
            if found is not None:
                self._invoke_event('on_message_delete', found)
                self.messages.remove(found)
        elif event == 'MESSAGE_UPDATE':
            older_message = self._get_message(data.get('id'))
            if older_message is not None:
                # create a copy of the new message
                message = copy.deepcopy(older_message)
                # update the new update
                for attr in data:
                    if attr == 'channel_id' or attr == 'author':
                        continue
                    value = data[attr]
                    if 'time' in attr:
                        setattr(message, attr, utils.parse_time(value))
                    else:
                        setattr(message, attr, value)
                self._invoke_event('on_message_edit', older_message, message)
                # update the older message
                older_message = message

        elif event == 'PRESENCE_UPDATE':
            server = self._get_server(data.get('guild_id'))
            if server is not None:
                status = data.get('status')
                user = data['user']
                member_id = user['id']
                member = utils.find(lambda m: m.id == member_id, server.members)
                if member is not None:
                    member.status = data.get('status')
                    member.game_id = data.get('game_id')
                    member.name = user.get('username', member.name)
                    member.avatar = user.get('avatar', member.avatar)

                    # call the event now
                    self._invoke_event('on_status', member)
                    self._invoke_event('on_member_update', member)
        elif event == 'USER_UPDATE':
            self.user = User(**data)
        elif event == 'CHANNEL_DELETE':
            server =  self._get_server(data.get('guild_id'))
            if server is not None:
                channel_id = data.get('id')
                channel = utils.find(lambda c: c.id == channel_id, server.channels)
                server.channels.remove(channel)
                self._invoke_event('on_channel_delete', channel)
        elif event == 'CHANNEL_UPDATE':
            server = self._get_server(data.get('guild_id'))
            if server is not None:
                channel_id = data.get('id')
                channel = utils.find(lambda c: c.id == channel_id, server.channels)
                channel.update(server=server, **data)
                self._invoke_event('on_channel_update', channel)
        elif event == 'CHANNEL_CREATE':
            is_private = data.get('is_private', False)
            channel = None
            if is_private:
                recipient = User(**data.get('recipient'))
                pm_id = data.get('id')
                channel = PrivateChannel(id=pm_id, user=recipient)
                self.private_channels.append(channel)
            else:
                server = self._get_server(data.get('guild_id'))
                if server is not None:
                    channel = Channel(server=server, **data)
                    server.channels.append(channel)

            self._invoke_event('on_channel_create', channel)
        elif event == 'GUILD_MEMBER_ADD':
            server = self._get_server(data.get('guild_id'))
            member = Member(server=server, deaf=False, mute=False, **data)
            server.members.append(member)
            self._invoke_event('on_member_join', member)
        elif event == 'GUILD_MEMBER_REMOVE':
            server = self._get_server(data.get('guild_id'))
            user_id = data['user']['id']
            member = utils.find(lambda m: m.id == user_id, server.members)
            server.members.remove(member)
            self._invoke_event('on_member_remove', member)
        elif event == 'GUILD_MEMBER_UPDATE':
            server = self._get_server(data.get('guild_id'))
            user_id = data['user']['id']
            member = utils.find(lambda m: m.id == user_id, server.members)
            if member is not None:
                user = data['user']
                member.name = user['username']
                member.discriminator = user['discriminator']
                member.avatar = user['avatar']
                member.roles = []
                # update the roles
                for role in server.roles:
                    if role.id in data['roles']:
                        member.roles.append(role)

                self._invoke_event('on_member_update', member)
        elif event == 'GUILD_CREATE':
            self._add_server(data)
            self._invoke_event('on_server_create', self.servers[-1])
        elif event == 'GUILD_DELETE':
            server = self._get_server(data.get('id'))
            self.servers.remove(server)
            self._invoke_event('on_server_delete', server)

    def _opened(self):
        log.info('Opened at {}'.format(int(time.time())))

    def _closed(self, code, reason=None):
        log.info('Closed with {} ("{}") at {}'.format(code, reason, int(time.time())))
        self._invoke_event('on_disconnect')

    def run(self):
        """Runs the client and allows it to receive messages and events."""
        log.info('Client is being run')
        self.ws.run_forever()

    @property
    def is_logged_in(self):
        """Returns True if the client is successfully logged in. False otherwise."""
        return self._is_logged_in

    def get_channel(self, id):
        """Returns a :class:`Channel` or :class:`PrivateChannel` with the following ID. If not found, returns None."""
        if id is None:
            return None

        for server in self.servers:
            for channel in server.channels:
                if channel.id == id:
                    return channel

        for pm in self.private_channels:
            if pm.id == id:
                return pm

    def start_private_message(self, user):
        """Starts a private message with the user. This allows you to :meth:`send_message` to it.

        Note that this method should rarely be called as :meth:`send_message` does it automatically.

        :param user: A :class:`User` to start the private message with.
        """
        if not isinstance(user, User):
            raise TypeError('user argument must be a User')

        payload = {
            'recipient_id': user.id
        }

        r = requests.post('{}/{}/channels'.format(endpoints.USERS, self.user.id), json=payload, headers=self.headers)
        if is_response_successful(r):
            data = r.json()
            log.debug(request_success_log.format(name='start_private_message', response=r, json=payload, data=data))
            self.private_channels.append(PrivateChannel(id=data['id'], user=user))
        else:
            log.error(request_logging_format.format(name='start_private_message', response=r))

    def send_message(self, destination, content, mentions=True, tts=False):
        """Sends a message to the destination given with the content given.

        The destination could be a :class:`Channel` or a :class:`PrivateChannel`. For convenience
        it could also be a :class:`User`. If it's a :class:`User` or :class:`PrivateChannel` then it
        sends the message via private message, otherwise it sends the message to the channel.

        The content must be a type that can convert to a string through ``str(content)``.

        The mentions must be either an array of :class:`User` to mention or a boolean. If
        ``mentions`` is ``True`` then all the users mentioned in the content are mentioned, otherwise
        no one is mentioned. Note that to mention someone in the content, you should use :meth:`User.mention`.

        :param destination: The location to send the message.
        :param content: The content of the message to send.
        :param mentions: A list of :class:`User` to mention in the message or a boolean. Ignored for private messages.
        :param tts: If ``True``, sends tries to send the message using text-to-speech.
        :return: The :class:`Message` sent or None if error occurred.
        """

        channel_id = ''
        is_private_message = True
        if isinstance(destination, Channel) or isinstance(destination, PrivateChannel):
            channel_id = destination.id
            is_private_message = destination.is_private
        elif isinstance(destination, User):
            found = utils.find(lambda pm: pm.user == destination, self.private_channels)
            if found is None:
                # Couldn't find the user, so start a PM with them first.
                self.start_private_message(destination)
                channel_id = self.private_channels[-1].id
            else:
                channel_id = found.id
        else:
            raise InvalidDestination('Destination must be Channel, PrivateChannel, or User')

        content = str(content)
        mentions = self._resolve_mentions(content, mentions)

        url = '{base}/{id}/messages'.format(base=endpoints.CHANNELS, id=channel_id)
        payload = {
            'content': content,
        }

        if not is_private_message:
            payload['mentions'] = mentions

        if tts:
            payload['tts'] = True

        response = requests.post(url, json=payload, headers=self.headers)
        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='send_message', response=response, json=payload, data=data))
            channel = self.get_channel(data.get('channel_id'))
            message = Message(channel=channel, **data)
            return message
        else:
            log.error(request_logging_format.format(name='send_message', response=response))

    def delete_message(self, message):
        """Deletes a :class:`Message`.

        Your own messages could be deleted without any proper permissions. However to
        delete other people's messages, you need the proper permissions to do so.

        :param message: The :class:`Message` to delete.
        :returns: True if the message was deleted successfully, False otherwise.
        """

        url = '{}/{}/messages/{}'.format(endpoints.CHANNELS, message.channel.id, message.id)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(name='delete_message', response=response))
        return is_response_successful(response)

    def edit_message(self, message, new_content, mentions=True):
        """Edits a :class:`Message` with the new message content.

        The new_content must be able to be transformed into a string via ``str(new_content)``.

        :param message: The :class:`Message` to edit.
        :param new_content: The new content to replace the message with.
        :param mentions: The mentions for the user. Same as :meth:`send_message`.
        :return: The new edited message or None if an error occurred.
        """

        channel = message.channel
        content = str(new_content)

        url = '{}/{}/messages/{}'.format(endpoints.CHANNELS, channel.id, message.id)
        payload = {
            'content': content
        }

        if not channel.is_private:
            payload['mentions'] = self._resolve_mentions(content, mentions)

        response = requests.patch(url, headers=self.headers, json=payload)
        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='edit_message', response=response, json=payload, data=data))
            return Message(channel=channel, **data)
        else:
            log.error(request_logging_format.format(name='edit_message', response=response))

    def login(self, email, password):
        """Logs in the user with the following credentials and initialises
        the connection to Discord.

        After this function is called, :attr:`is_logged_in` returns True if no
        errors occur.

        :param str email: The email used to login.
        :param str password: The password used to login.
        """

        payload = {
            'email': email,
            'password': password
        }

        r = requests.post(endpoints.LOGIN, json=payload)

        if is_response_successful(r):
            log.info('logging in returned status code {}'.format(r.status_code))
            self.email = email

            body = r.json()
            self.token = body['token']
            self.headers['authorization'] = self.token

            gateway = requests.get(endpoints.GATEWAY, headers=self.headers)
            if not is_response_successful(gateway):
                raise GatewayNotFound()
            self._create_websocket(gateway.json().get('url'), reconnect=False)
            self._is_logged_in = True
        else:
            log.error(request_logging_format.format(name='login', response=r))

    def logout(self):
        """Logs out of Discord and closes all connections."""
        response = requests.post(endpoints.LOGOUT)
        self.ws.close()
        self._is_logged_in = False
        self.keep_alive.stop.set()
        log.debug(request_logging_format.format(name='logout', response=response))

    def logs_from(self, channel, limit=500):
        """A generator that obtains logs from a specified channel.

        Yielding from the generator returns a :class:`Message` object with the message data.

        Example: ::

            for message in client.logs_from(channel):
                if message.content.startswith('!hello'):
                    if message.author == client.user:
                        client.edit_message(message, 'goodbye')


        :param channel: The :class:`Channel` to obtain the logs from.
        :param limit: The number of messages to retrieve.
        """

        url = '{}/{}/messages'.format(endpoints.CHANNELS, channel.id)
        params = {
            'limit': limit
        }
        response = requests.get(url, params=params, headers=self.headers)
        if is_response_successful(response):
            messages = response.json()
            log.info('logs_from: {0.url} was successful'.format(response))
            for message in messages:
                yield Message(channel=channel, **message)
        else:
            log.error(request_logging_format.format(name='logs_from', response=response))

    def event(self, function):
        """A decorator that registers an event to listen to.

        You can find more info about the events on the :ref:`documentation below <discord-api-events>`.

        Example: ::

            @client.event
            def on_ready():
                print('Ready!')
        """

        if function.__name__ not in self.events:
            raise InvalidEventName('The function name {} is not a valid event name'.format(function.__name__))

        self.events[function.__name__] = function
        log.info('{0.__name__} has successfully been registered as an event'.format(function))
        return function

    def delete_channel(self, channel):
        """Deletes a channel.

        In order to delete the channel, the client must have the proper permissions
        in the server the channel belongs to.

        :param channel: The :class:`Channel` to delete.
        :returns: True if channel was deleted successfully, False otherwise.
        """

        url = '{}/{}'.format(endpoints.CHANNELS, channel.id)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='delete_channel'))
        return is_response_successful(response)

    def kick(self, server, user):
        """Kicks a :class:`User` from their respective :class:`Server`.

        You must have the proper permissions to kick a user in the server.

        :param server: The :class:`Server` to kick the member from.
        :param user: The :class:`User` to kick.
        :returns: True if kick was successful, False otherwise.
        """

        url = '{base}/{server}/members/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='kick'))
        return is_response_successful(response)

    def ban(self, server, user):
        """Bans a :class:`User` from their respective :class:`Server`.

        You must have the proper permissions to ban a user in the server.

        :param server: The :class:`Server` to ban the member from.
        :param user: The :class:`User` to ban.
        :returns: True if ban was successful, False otherwise.
        """

        url = '{base}/{server}/bans/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
        response = requests.put(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='ban'))
        return is_response_successful(response)

    def unban(self, server, name):
        """Unbans a :class:`User` from their respective :class:`Server`.

        You must have the proper permissions to unban a user in the server.

        :param server: The :class:`Server` to unban the member from.
        :param user: The :class:`User` to unban.
        :returns: True if unban was successful, False otherwise.
        """

        url = '{base}/{server}/bans/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='unban'))
        return is_response_successful(response)

    def edit_profile(self, password, **fields):
        """Edits the current profile of the client.

        All fields except password are optional.

        :param password: The current password for the client's account.
        :param new_password: The new password you wish to change to.
        :param email: The new email you wish to change to.
        :param username: The new username you wish to change to.
        """

        payload = {
            'password': password,
            'new_password': fields.get('new_password'),
            'email': fields.get('email', self.email),
            'username': fields.get('username', self.user.name),
            'avatar': self.user.avatar
        }

        url = '{0}/@me'.format(endpoints.USERS)
        response = requests.patch(url, headers=self.headers, json=payload)

        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='edit_profile', response=response, json=payload, data=data))
            self.token = data['token']
            self.email = data['email']
            self.headers['authorization'] = self.token
            self.user = User(**data)
        else:
            log.debug(request_logging_format.format(response=response, name='edit_profile'))

    def edit_channel(self, channel, **options):
        """Edits a :class:`Channel`.

        You must have the proper permissions to edit the channel.

        References pointed to the channel will be updated with the new information.

        :param channel: The :class:`Channel` to update.
        :param name: The new channel name.
        :param position: The new channel's position in the GUI.
        :param topic: The new channel's topic.
        :returns: True if editing was successful, False otherwise.
        """

        url = '{0}/{1.id}'.format(endpoints.CHANNELS, channel)
        payload = {
            'name': options.get('name', channel.name),
            'topic': options.get('topic', channel.topic),
            'position': options.get('position', channel.position)
        }

        response = requests.patch(url, headers=self.headers, json=payload)
        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='edit_channel', response=response, json=payload, data=data))
            channel.update(server=channel.server, **data)
            return True
        else:
            log.debug(request_logging_format.format(response=response, name='edit_channel'))
            return False

    def create_channel(self, server, name, type='text'):
        """Creates a :class:`Channel` in the specified :class:`Server`.

        Note that you need the proper permissions to create the channel.

        :param server: The :class:`Server` to create the channel in.
        :param name: The channel's name.
        :param type: The type of channel to create. 'text' or 'voice'.
        :returns: The newly created :class:`Channel` if successful, else None.
        """

        payload = {
            'name': name,
            'type': type
        }

        url = '{0}/{1.id}/channels'.format(endpoints.SERVERS, server)
        response = requests.post(url, headers=self.headers, json=payload)
        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='create_channel', response=response, data=data, json=payload))
            channel = Channel(server=server, **data)
            # We don't append it to server.channels because CHANNEL_CREATE handles it for us.
            return channel
        else:
            log.debug(request_logging_format.format(response=response, name='create_channel'))

    def leave_server(self, server):
        """Leaves a :class:`Server`.

        :param server: The :class:`Server` to leave.
        :returns: True if leaving was successful, False otherwise.
        """

        url = '{0}/{1.id}'.format(endpoints.SERVERS, server)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='leave_server'))
        return is_response_successful(response)

    def create_invite(self, destination, **options):
        """Creates an invite for the destination which could be either a :class:`Server` or :class:`Channel`.

        The available options are:

        :param destination: The :class:`Server` or :class:`Channel` to create the invite to.
        :param max_age: How long the invite should last. If it's 0 then the invite doesn't expire. Defaults to 0.
        :param max_uses: How many uses the invite could be used for. If it's 0 then there are unlimited uses. Defaults to 0.
        :param temporary: A boolean to denote that the invite grants temporary membership (i.e. they get kicked after they disconnect). Defaults to False.
        :param xkcd: A boolean to indicate if the invite URL is human readable. Defaults to False.
        :returns: The :class:`Invite` if creation is successful, None otherwise.
        """

        payload = {
            'max_age': options.get('max_age', 0),
            'max_uses': options.get('max_uses', 0),
            'temporary': options.get('temporary', False),
            'xkcdpass': options.get('xkcd', False)
        }

        url = '{0}/{1.id}/invites'.format(endpoints.CHANNELS, destination)
        response = requests.post(url, headers=self.headers, json=payload)
        if is_response_successful(response):
            data = response.json()
            log.debug(request_success_log.format(name='create_invite', json=payload, response=response, data=data))
            data['server'] = self._get_server(data['guild']['id'])
            channel_id = data['channel']['id']
            data['channel'] = utils.find(lambda ch: ch.id == channel_id, data['server'].channels)
            return Invite(**data)
        else:
            log.debug(request_logging_format.format(response=response, name='create_invite'))

    def accept_invite(self, invite):
        """Accepts an :class:`Invite` or a URL to an invite.

        The URL must be a discord.gg URL. e.g. "http://discord.gg/codehere"

        :param invite: The :class:`Invite` or URL to an invite to accept.
        :returns: True if the invite was successfully accepted, False otherwise.
        """

        destination = None
        if isinstance(invite, Invite):
            destination = invite.id
        else:
            rx = r'(?:https?\:\/\/)?discord\.gg\/(.+)'
            m = re.match(rx, invite)
            if m:
                destination = m.group(1)

        if destination is None:
            return False

        url = '{0}/invite/{1}'.format(endpoints.API_BASE, destination)
        response = requests.post(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='accept_invite'))
        return is_response_successful(response)

    def edit_role(self, server, role):
        """Edits the specified :class:`Role` for the entire :class:`Server`.

        To use this you have to edit the role yourself and then pass it
        to this member function. For example: ::

            server = message.channel.server
            role = find(lambda r: r.name == 'My Cool Role', server.roles)
            role.name = 'My Not So Cool Role'
            role.permissions.can_kick_members = False
            role.permissions.can_ban_members = False
            client.edit_role(server, role)

        Note that you cannot edit the name of the @everyone role as that role is special.

        :param server: The :class:`Server` the role belongs to.
        :param role: The :class:`Role` to edit.
        :return: ``True`` if editing was successful, ``False`` otherwise.
        """

        url = '{0}/{1.id}/roles/{2.id}'.format(endpoints.SERVERS, server, role)

        payload = {
            'name': role.name,
            'permissions': role.permissions.value
        }

        response = requests.patch(url, json=payload, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='edit_role'))
        return is_response_successful(response)

    def delete_role(self, server, role):
        """Deletes the specified :class:`Role` for the entire :class:`Server`.

        Works in a similar matter to :func:`edit_role`.

        :param server: The :class:`Server` the role belongs to.
        :param role: The :class:`Role` to delete.
        :return: ``True`` if deleting was successful, ``False`` otherwise.
        """

        url = '{0}/{1.id}/roles/{2.id}'.format(endpoints.SERVERS, server, role)
        response = requests.delete(url, headers=self.headers)
        log.debug(request_logging_format.format(response=response, name='delete_role'))
        return is_response_successful(response)
Example #7
0
class Client(object):
    """Represents a client connection that connects to Discord.
    This class is used to interact with the Discord WebSocket and API.

    A number of options can be passed to the :class:`Client` via keyword arguments.

    :param int max_length: The maximum number of messages to store in :attr:`messages`. Defaults to 5000.

    Instance attributes:

     .. attribute:: user

         A :class:`User` that represents the connected client. None if not logged in.
     .. attribute:: servers

         A list of :class:`Server` that the connected client has available.
     .. attribute:: private_channels

         A list of :class:`PrivateChannel` that the connected client is participating on.
     .. attribute:: messages

        A deque_ of :class:`Message` that the client has received from all servers and private messages.

    .. _deque: https://docs.python.org/3.4/library/collections.html#collections.deque
    """

    def __init__(self, **kwargs):
        self._is_logged_in = False
        self.user = None
        self.servers = []
        self.private_channels = []
        self.token = ""
        self.messages = deque([], maxlen=kwargs.get("max_length", 5000))
        self.events = {
            "on_ready": _null_event,
            "on_disconnect": _null_event,
            "on_error": _null_event,
            "on_response": _null_event,
            "on_message": _null_event,
            "on_message_delete": _null_event,
            "on_message_edit": _null_event,
            "on_status": _null_event,
            "on_channel_delete": _null_event,
            "on_channel_create": _null_event,
            "on_member_join": _null_event,
            "on_member_remove": _null_event,
            "on_member_typing": _null_event,
        }

        gateway = requests.get(endpoints.GATEWAY)
        if gateway.status_code != 200:
            raise GatewayNotFound()
        gateway_js = gateway.json()
        url = gateway_js.get("url")
        if url is None:
            raise GatewayNotFound()

        self.ws = WebSocketClient(url, protocols=["http-only", "chat"])

        # this is kind of hacky, but it's to avoid deadlocks.
        # i.e. python does not allow me to have the current thread running if it's self
        # it throws a 'cannot join current thread' RuntimeError
        # So instead of doing a basic inheritance scheme, we're overriding the member functions.

        self.ws.opened = self._opened
        self.ws.closed = self._closed
        self.ws.received_message = self._received_message

        # the actual headers for the request...
        # we only override 'authorization' since the rest could use the defaults.
        self.headers = {"authorization": self.token}

    def _get_message(self, msg_id):
        return next((m for m in self.messages if m.id == msg_id), None)

    def _get_server(self, guild_id):
        return next((s for s in self.servers if s.id == guild_id), None)

    def _get_channel(self, channel_id):
        return next((c for c in server.channels if c.id == channel_id), None)

    def _get_member(self, user_id):
        return next((m for m in server.members if m.id == user_id), None)

    def _resolve_mentions(self, content, mentions):
        if isinstance(mentions, list):
            return [user.id for user in mentions]
        elif mentions == True:
            return re.findall(r"@<(\d+)>", content)
        else:
            return []

    def _invoke_event(self, event_name, *args, **kwargs):
        try:
            self.events[event_name](*args, **kwargs)
        except Exception as e:
            pass

    def _received_message(self, msg):
        response = json.loads(str(msg))
        if response.get("op") != 0:
            return

        self._invoke_event("on_response", response)
        event = response.get("t")
        data = response.get("d")

        if event == "READY":
            self.user = User(**data["user"])
            guilds = data.get("guilds")

            for guild in guilds:
                guild["roles"] = [Role(**role) for role in guild["roles"]]
                members = guild["members"]
                for i, member in enumerate(members):
                    roles = member["roles"]
                    for j, roleid in enumerate(roles):
                        role = next((r for r in guild["roles"] if r.id == roleid), None)
                        if role is not None:
                            roles[j] = role
                    members[i] = Member(**member)

                for presence in guild["presences"]:
                    user_id = presence["user"]["id"]
                    member = next((m for m in members if m.id == user_id), None)
                    if member is not None:
                        member.status = presence["status"]
                        member.game_id = presence["game_id"]

                server = Server(**guild)

                # give all the members their proper server
                for member in server.members:
                    member.server = server

                for channel in guild["channels"]:
                    changed_roles = []
                    permission_overwrites = channel["permission_overwrites"]

                    for overridden in permission_overwrites:
                        # this is pretty inefficient due to the deep nested loops unfortunately
                        role = next((role for role in guild["roles"] if role.id == overridden["id"]), None)
                        if role is None:
                            continue
                        denied = overridden.get("deny", 0)
                        allowed = overridden.get("allow", 0)
                        override = copy.deepcopy(role)

                        # Basically this is what's happening here.
                        # We have an original bit array, e.g. 1010
                        # Then we have another bit array that is 'denied', e.g. 1111
                        # And then we have the last one which is 'allowed', e.g. 0101
                        # We want original OP denied to end up resulting in whatever is in denied to be set to 0.
                        # So 1010 OP 1111 -> 0000
                        # Then we take this value and look at the allowed values. And whatever is allowed is set to 1.
                        # So 0000 OP2 0101 -> 0101
                        # The OP is (base ^ denied) & ~denied.
                        # The OP2 is base | allowed.
                        override.permissions.value = ((override.permissions.value ^ denied) & ~denied) | allowed
                        changed_roles.append(override)

                    channel["permission_overwrites"] = changed_roles
                channels = [Channel(server=server, **channel) for channel in guild["channels"]]
                server.channels = channels
                self.servers.append(server)

            for pm in data.get("private_channels"):
                self.private_channels.append(PrivateChannel(id=pm["id"], user=User(**pm["recipient"])))

            # set the keep alive interval..
            interval = data.get("heartbeat_interval") / 1000.0
            self.keep_alive = _keep_alive_handler(interval, self.ws)

            # we're all ready
            self._invoke_event("on_ready")
        elif event == "MESSAGE_CREATE":
            channel = self.get_channel(data.get("channel_id"))
            message = Message(channel=channel, **data)
            self.events["on_message"](message)
            self.messages.append(message)
        elif event == "MESSAGE_DELETE":
            channel = self.get_channel(data.get("channel_id"))
            message_id = data.get("id")
            found = self._get_message(message_id)
            if found is not None:
                self.events["on_message_delete"](found)
                self.messages.remove(found)
        elif event == "MESSAGE_UPDATE":
            older_message = self._get_message(data.get("id"))
            if older_message is not None:
                # create a copy of the new message
                message = copy.deepcopy(older_message)
                # update the new update
                for attr in data:
                    if attr == "channel_id":
                        continue
                    value = data[attr]
                    if "time" in attr:
                        setattr(message, attr, message._parse_time(value))
                    else:
                        setattr(message, attr, value)
                self._invoke_event("on_message_edit", older_message, message)
                # update the older message
                older_message = message

        elif event == "PRESENCE_UPDATE":
            server = self._get_server(data.get("guild_id"))
            if server is not None:
                status = data.get("status")
                member_id = data["user"]["id"]
                member = next((u for u in server.members if u.id == member_id), None)
                if member is not None:
                    member.status = data.get("status")
                    member.game_id = data.get("game_id")
                    # call the event now
                    self._invoke_event("on_status", member)
        elif event == "USER_UPDATE":
            self.user = User(**data)
        elif event == "CHANNEL_DELETE":
            server = self._get_server(data.get("guild_id"))
            if server is not None:
                channel_id = data.get("id")
                channel = next((c for c in server.channels if c.id == channel_id), None)
                server.channels.remove(channel)
                self._invoke_event("on_channel_delete", channel)
        elif event == "CHANNEL_CREATE":
            is_private = data.get("is_private", False)
            channel = None
            if is_private:
                recipient = User(**data.get("recipient"))
                pm_id = data.get("id")
                channel = PrivateChannel(id=pm_id, user=recipient)
                self.private_channels.append(channel)
            else:
                server = self._get_server(data.get("guild_id"))
                if server is not None:
                    channel = Channel(server=server, **data)
                    server.channels.append(channel)

            self._invoke_event("on_channel_create", channel)
        elif event == "GUILD_MEMBER_ADD":
            server = self._get_server(data.get("guild_id"))
            member = Member(deaf=False, mute=False, **data)
            server.members.append(member)
            self._invoke_event("on_member_join", member)
        elif event == "GUILD_MEMBER_REMOVE":
            server = self._get_server(data.get("guild_id"))
            user_id = data["user"]["id"]
            member = next((m for m in server.members if m.id == user_id), None)
            server.members.remove(member)
            self._invoke_event("on_member_remove", member)
        elif event == "TYPING_START":
            if self.events["on_member_typing"] == _null_event:
                return

            member = self._get_member(data["user_id"])
            channel = self._get_channel(data["channel_id"])
            timestamp = data["timestamp"]
            self._invoke_event("on_member_remove", member, channel, timestamp)
        # elif event == 'VOICE_STATE_UPDATE':
        #   pass
        # else:
        #    print("Unhandled event (%s)" % event)
        #    from pprint import pprint
        #    pprint(response)

    def _opened(self):
        print("Opened at {}".format(int(time.time())))

    def _closed(self, code, reason=None):
        print('Closed with {} ("{}") at {}'.format(code, reason, int(time.time())))
        self._invoke_event("on_disconnect")

    def run(self):
        """Runs the client and allows it to receive messages and events."""
        self.ws.run_forever()

    @property
    def is_logged_in(self):
        """Returns True if the client is successfully logged in. False otherwise."""
        return self._is_logged_in

    def get_channel(self, id):
        """Returns a :class:`Channel` or :class:`PrivateChannel` with the following ID. If not found, returns None."""
        if id is None:
            return None

        for server in self.servers:
            for channel in server.channels:
                if channel.id == id:
                    return channel

        for pm in self.private_channels:
            if pm.id == id:
                return pm

    def start_private_message(self, user):
        """Starts a private message with the user. This allows you to :meth:`send_message` to it.

        Note that this method should rarely be called as :meth:`send_message` does it automatically.

        :param user: A :class:`User` to start the private message with.
        """
        if not isinstance(user, User):
            raise TypeError("user argument must be a User")

        payload = {"recipient_id": user.id}

        r = requests.post("{}/{}/channels".format(endpoints.USERS, self.user.id), json=payload, headers=self.headers)
        if r.status_code == 200:
            data = r.json()
            self.private_channels.append(PrivateChannel(id=data["id"], user=user))

    def send_message(self, destination, content, mentions=True):
        """Sends a message to the destination given with the content given.

        The destination could be a :class:`Channel` or a :class:`PrivateChannel`. For convenience
        it could also be a :class:`User`. If it's a :class:`User` or :class:`PrivateChannel` then it
        sends the message via private message, otherwise it sends the message to the channel.

        The content must be a type that can convert to a string through ``str(content)``.

        The mentions must be either an array of :class:`User` to mention or a boolean. If
        ``mentions`` is ``True`` then all the users mentioned in the content are mentioned, otherwise
        no one is mentioned. Note that to mention someone in the content, you should use :meth:`User.mention`.

        :param destination: The location to send the message.
        :param content: The content of the message to send.
        :param mentions: A list of :class:`User` to mention in the message or a boolean. Ignored for private messages.
        :return: The :class:`Message` sent or None if error occurred.
        """

        channel_id = ""
        is_private_message = True
        if isinstance(destination, Channel) or isinstance(destination, PrivateChannel):
            channel_id = destination.id
            is_private_message = destination.is_private
        elif isinstance(destination, User):
            found = next((pm for pm in self.private_channels if pm.user == destination), None)
            if found is None:
                # Couldn't find the user, so start a PM with them first.
                self.start_private_message(destination)
                channel_id = self.private_channels[-1].id
            else:
                channel_id = found.id
        else:
            raise InvalidDestination("Destination must be Channel, PrivateChannel, or User")

        content = str(content)
        mentions = self._resolve_mentions(content, mentions)

        url = "{base}/{id}/messages".format(base=endpoints.CHANNELS, id=channel_id)
        payload = {"content": content}

        if not is_private_message:
            payload["mentions"] = mentions

        response = requests.post(url, json=payload, headers=self.headers)
        if response.status_code == 200:
            data = response.json()
            channel = self.get_channel(data.get("channel_id"))
            message = Message(channel=channel, **data)
            return message

    def delete_message(self, message):
        """Deletes a :class:`Message`

        A fairly straightforward function.

        :param message: The :class:`Message` to delete.
        """
        url = "{}/{}/messages/{}".format(endpoints.CHANNELS, message.channel.id, message.id)
        response = requests.delete(url, headers=self.headers)

    def edit_message(self, message, new_content, mentions=True):
        """Edits a :class:`Message` with the new message content.

        The new_content must be able to be transformed into a string via ``str(new_content)``.

        :param message: The :class:`Message` to edit.
        :param new_content: The new content to replace the message with.
        :param mentions: The mentions for the user. Same as :meth:`send_message`.
        :return: The new edited message or None if an error occurred."""
        channel = message.channel
        content = str(new_content)

        url = "{}/{}/messages/{}".format(endpoints.CHANNELS, channel.id, message.id)
        payload = {"content": content}

        if not channel.is_private:
            payload["mentions"] = self._resolve_mentions(content, mentions)

        response = requests.patch(url, headers=self.headers, json=payload)
        if response.status_code == 200:
            data = response.json()
            return Message(channel=channel, **data)

    def login(self, email, password):
        """Logs in the user with the following credentials and initialises
        the connection to Discord.

        After this function is called, :attr:`is_logged_in` returns True if no
        errors occur.

        :param str email: The email used to login.
        :param str password: The password used to login.
        """

        self.ws.connect()

        payload = {"email": email, "password": password}

        r = requests.post(endpoints.LOGIN, json=payload)

        if r.status_code == 200:
            body = r.json()
            self.token = body["token"]
            self.headers["authorization"] = self.token
            second_payload = {
                "op": 2,
                "d": {
                    "token": self.token,
                    "properties": {
                        "$os": sys_platform,
                        "$browser": "discord.py",
                        "$device": "discord.py",
                        "$referrer": "",
                        "$referring_domain": "",
                    },
                    "v": 2,
                },
            }

            self.ws.send(json.dumps(second_payload))
            self._is_logged_in = True

    def logout(self):
        """Logs out of Discord and closes all connections."""
        response = requests.post(endpoints.LOGOUT)
        self.ws.close()
        self._is_logged_in = False
        self.keep_alive.cancel()

    def logs_from(self, channel, limit=500):
        """A generator that obtains logs from a specified channel.

        Yielding from the generator returns a :class:`Message` object with the message data.

        Example: ::

            for message in client.logs_from(channel):
                if message.content.startswith('!hello'):
                    if message.author == client.user:
                        client.edit_message(message, 'goodbye')


        :param channel: The :class:`Channel` to obtain the logs from.
        :param limit: The number of messages to retrieve.
        """

        url = "{}/{}/messages".format(endpoints.CHANNELS, channel.id)
        params = {"limit": limit}
        response = requests.get(url, params=params, headers=self.headers)
        if response.status_code == 200:
            messages = response.json()
            for message in messages:
                yield Message(channel=channel, **message)

    def event(self, function):
        """A decorator that registers an event to listen to.

        You can find more info about the events on the :ref:`documentation below <discord-api-events>`.

        Example: ::

            @client.event
            def on_ready():
                print('Ready!')
        """

        if function.__name__ not in self.events:
            raise InvalidEventName("The function name {} is not a valid event name".format(function.__name__))

        self.events[function.__name__] = function
        return function

    def create_channel(self, server, name, type="text"):
        """Creates a channel in the specified server.

        In order to create the channel the client must have the proper permissions.

        :param server: The :class:`Server` to create the channel from.
        :param name: The name of the channel to create.
        :param type: The type of channel to create. Valid values are 'text' or 'voice'.
        :returns: The recently created :class:`Channel`.
        """
        url = "{}/{}/channels".format(endpoints.SERVERS, server.id)
        payload = {"name": name, "type": type}
        response = requests.post(url, json=payload, headers=self.headers)
        if response.status_code == 200:
            channel = Channel(server=server, **response.json())
            return channel

    def delete_channel(self, channel):
        """Deletes a channel.

        In order to delete the channel, the client must have the proper permissions
        in the server the channel belongs to.

        :param channel: The :class:`Channel` to delete.
        """
        url = "{}/{}".format(endpoints.CHANNELS, channel.id)
        response = requests.delete(url, headers=self.headers)