Example #1
0
class State(object):
    """
    The State class is used to track global state based on events emitted from
    the `GatewayClient`. State tracking is a core component of the Disco client,
    providing the mechanism for most of the higher-level utility functions.

    Attributes
    ----------
    EVENTS : list(str)
        A list of all events the State object binds to
    client : `disco.client.Client`
        The Client instance this state is attached to
    config : `StateConfig`
        The configuration for this state instance
    me : `User`
        The currently logged in user
    dms : dict(snowflake, `Channel`)
        Mapping of all known DM Channels
    guilds : dict(snowflake, `Guild`)
        Mapping of all known/loaded Guilds
    channels : dict(snowflake, `Channel`)
        Weak mapping of all known/loaded Channels
    users : dict(snowflake, `User`)
        Weak mapping of all known/loaded Users
    voice_clients : dict(str, 'VoiceClient')
        Weak mapping of all known voice clients
    voice_states : dict(str, `VoiceState`)
        Weak mapping of all known/active Voice States
    messages : Optional[dict(snowflake, deque)]
        Mapping of channel ids to deques containing `StackMessage` objects
    """
    EVENTS = [
        'Ready',
        'GuildCreate',
        'GuildUpdate',
        'GuildDelete',
        'GuildMemberAdd',
        'GuildMemberRemove',
        'GuildMemberUpdate',
        'GuildMembersChunk',
        'GuildRoleCreate',
        'GuildRoleUpdate',
        'GuildRoleDelete',
        'GuildEmojisUpdate',
        'ChannelCreate',
        'ChannelUpdate',
        'ChannelDelete',
        'VoiceServerUpdate',
        'VoiceStateUpdate',
        'MessageCreate',
        'PresenceUpdate',
    ]

    def __init__(self, client, config):
        self.client = client
        self.config = config

        self.ready = Event()
        self.guilds_waiting_sync = 0

        self.me = None
        self.dms = HashMap()
        self.guilds = HashMap()
        self.channels = HashMap(weakref.WeakValueDictionary())
        self.users = HashMap(weakref.WeakValueDictionary())
        self.voice_clients = HashMap(weakref.WeakValueDictionary())
        self.voice_states = HashMap(weakref.WeakValueDictionary())

        # If message tracking is enabled, listen to those events
        if self.config.track_messages:
            self.messages = DefaultHashMap(
                lambda: deque(maxlen=self.config.track_messages_size))
            self.EVENTS += ['MessageDelete', 'MessageDeleteBulk']

        # The bound listener objects
        self.listeners = []
        self.bind()

    def unbind(self):
        """
        Unbinds all bound event listeners for this state object.
        """
        map(lambda k: k.unbind(), self.listeners)
        self.listeners = []

    def bind(self):
        """
        Binds all events for this state object, storing the listeners for later
        unbinding.
        """
        assert not len(
            self.listeners), 'Binding while already bound is dangerous'

        for event in self.EVENTS:
            func = 'on_' + underscore(event)
            self.listeners.append(
                self.client.events.on(event,
                                      getattr(self, func),
                                      priority=Priority.BEFORE))

    def fill_messages(self, channel):
        for message in reversed(next(channel.messages_iter(bulk=True))):
            self.messages[channel.id].append(
                StackMessage(message.id, message.channel_id,
                             message.author.id))

    def on_ready(self, event):
        self.me = event.user
        self.guilds_waiting_sync = len(event.guilds)

        for dm in event.private_channels:
            self.dms[dm.id] = dm
            self.channels[dm.id] = dm

    def on_message_create(self, event):
        if self.config.track_messages:
            self.messages[event.message.channel_id].append(
                StackMessage(event.message.id, event.message.channel_id,
                             event.message.author.id))

        if event.message.channel_id in self.channels:
            self.channels[
                event.message.channel_id].last_message_id = event.message.id

    def on_message_delete(self, event):
        if event.channel_id not in self.messages:
            return

        sm = next(
            (i for i in self.messages[event.channel_id] if i.id == event.id),
            None)
        if not sm:
            return

        self.messages[event.channel_id].remove(sm)

    def on_message_delete_bulk(self, event):
        if event.channel_id not in self.messages:
            return

        # TODO: performance
        for sm in list(self.messages[event.channel_id]):
            if sm.id in event.ids:
                self.messages[event.channel_id].remove(sm)

    def on_guild_create(self, event):
        if event.unavailable is False:
            self.guilds_waiting_sync -= 1
            if self.guilds_waiting_sync <= 0:
                self.ready.set()

        self.guilds[event.guild.id] = event.guild
        self.channels.update(event.guild.channels)

        for member in six.itervalues(event.guild.members):
            if member.user.id not in self.users:
                self.users[member.user.id] = member.user

        for presence in event.presences:
            if presence.user.id in self.users:
                self.users[presence.user.id].presence = presence

        for voice_state in six.itervalues(event.guild.voice_states):
            self.voice_states[voice_state.session_id] = voice_state

        if self.config.sync_guild_members:
            event.guild.request_guild_members()

    def on_guild_update(self, event):
        self.guilds[event.guild.id].inplace_update(event.guild,
                                                   ignored=[
                                                       'channels',
                                                       'members',
                                                       'voice_states',
                                                       'presences',
                                                   ])

    def on_guild_delete(self, event):
        if event.id in self.guilds:
            # Just delete the guild, channel references will fail
            del self.guilds[event.id]

        if event.id in self.voice_clients:
            self.voice_clients[event.id].disconnect()

    def on_channel_create(self, event):
        if event.channel.is_guild and event.channel.guild_id in self.guilds:
            self.guilds[event.channel.guild_id].channels[
                event.channel.id] = event.channel
            self.channels[event.channel.id] = event.channel
        elif event.channel.is_dm:
            self.dms[event.channel.id] = event.channel
            self.channels[event.channel.id] = event.channel

    def on_channel_update(self, event):
        if event.channel.id in self.channels:
            self.channels[event.channel.id].inplace_update(event.channel)

            if event.overwrites is not UNSET:
                self.channels[event.channel.id].overwrites = event.overwrites
                self.channels[event.channel.id].after_load()

    def on_channel_delete(self, event):
        if event.channel.is_guild and event.channel.guild and event.channel.id in event.channel.guild.channels:
            del event.channel.guild.channels[event.channel.id]
        elif event.channel.is_dm and event.channel.id in self.dms:
            del self.dms[event.channel.id]

    def on_voice_server_update(self, event):
        if event.guild_id not in self.voice_clients:
            return

        voice_client = self.voice_clients.get(event.guild_id)
        voice_client.set_endpoint(event.endpoint)
        voice_client.set_token(event.token)

    def on_voice_state_update(self, event):
        # Existing connection, we are either moving channels or disconnecting
        if event.state.session_id in self.voice_states:
            # Moving channels
            if event.state.channel_id:
                self.voice_states[event.state.session_id].inplace_update(
                    event.state)
            # Disconnection
            else:
                if event.state.guild_id in self.guilds:
                    if event.state.session_id in self.guilds[
                            event.state.guild_id].voice_states:
                        del self.guilds[event.state.guild_id].voice_states[
                            event.state.session_id]
                del self.voice_states[event.state.session_id]
        # New connection
        elif event.state.channel_id:
            if event.state.guild_id in self.guilds:
                expired_voice_state = self.guilds[
                    event.state.guild_id].voice_states.select_one(
                        user_id=event.user_id)
                if expired_voice_state:
                    del self.guilds[event.state.guild_id].voice_states[
                        expired_voice_state.session_id]
                self.guilds[event.state.guild_id].voice_states[
                    event.state.session_id] = event.state
            expired_voice_state = self.voice_states.select_one(
                user_id=event.user_id)
            if expired_voice_state:
                del self.voice_states[expired_voice_state.session_id]
            self.voice_states[event.state.session_id] = event.state

        if event.state.user_id != self.me.id:
            return

        server_id = event.state.guild_id or event.state.channel_id
        if server_id in self.voice_clients:
            voice_client = self.voice_clients[server_id]

            voice_client.channel_id = event.state.channel_id
            if not event.state.channel_id:
                voice_client.disconnect()
                return

            if voice_client.token:
                voice_client.set_state(VoiceState.CONNECTED)

    def on_guild_member_add(self, event):
        if event.member.user.id not in self.users:
            self.users[event.member.user.id] = event.member.user
        else:
            event.member.user = self.users[event.member.user.id]

        if event.member.guild_id not in self.guilds:
            return

        self.guilds[event.member.guild_id].members[
            event.member.id] = event.member

    def on_guild_member_update(self, event):
        if event.member.guild_id not in self.guilds:
            return

        if event.member.id not in self.guilds[event.member.guild_id].members:
            return

        self.guilds[event.member.guild_id].members[
            event.member.id].inplace_update(event.member)

    def on_guild_member_remove(self, event):
        if event.guild_id not in self.guilds:
            return

        if event.user.id not in self.guilds[event.guild_id].members:
            return

        del self.guilds[event.guild_id].members[event.user.id]

    def on_guild_members_chunk(self, event):
        if event.guild_id not in self.guilds:
            return

        guild = self.guilds[event.guild_id]
        for member in event.members:
            member.guild_id = guild.id
            guild.members[member.id] = member

            if member.id not in self.users:
                self.users[member.id] = member.user
            else:
                member.user = self.users[member.id]

    def on_guild_role_create(self, event):
        if event.guild_id not in self.guilds:
            return

        self.guilds[event.guild_id].roles[event.role.id] = event.role

    def on_guild_role_update(self, event):
        if event.guild_id not in self.guilds:
            return

        self.guilds[event.guild_id].roles[event.role.id].inplace_update(
            event.role)

    def on_guild_role_delete(self, event):
        if event.guild_id not in self.guilds:
            return

        if event.role_id not in self.guilds[event.guild_id].roles:
            return

        del self.guilds[event.guild_id].roles[event.role_id]

    def on_guild_emojis_update(self, event):
        if event.guild_id not in self.guilds:
            return

        for emoji in event.emojis:
            emoji.guild_id = event.guild_id

        self.guilds[event.guild_id].emojis = HashMap(
            {i.id: i
             for i in event.emojis})

    def on_presence_update(self, event):
        # TODO: this is recursive, we hackfix in model, but its still lame ATM
        user = event.presence.user
        user.presence = event.presence

        # if we have the user tracked locally, we can just use the presence
        #  update to update both their presence and the cached user object.
        if user.id in self.users:
            self.users[user.id].inplace_update(user)
        else:
            # Otherwise this user does not exist in our local cache, so we can
            #  use this opportunity to add them. They will quickly fall out of
            #  scope and be deleted if they aren't used below
            self.users[user.id] = user

        # Some updates come with a guild_id and roles the user is in, we should
        #  use this to update the guild member, but only if we have the guild
        #  cached.
        if event.roles is UNSET or event.guild_id not in self.guilds:
            return

        if user.id not in self.guilds[event.guild_id].members:
            return

        self.guilds[event.guild_id].members[user.id].roles = event.roles
Example #2
0
class State(object):
    """
    The State class is used to track global state based on events emitted from
    the :class:`GatewayClient`. State tracking is a core component of the Disco
    client, providing the mechanism for most of the higher-level utility functions.

    Attributes
    ----------
    EVENTS : list(str)
        A list of all events the State object binds to
    client : :class:`disco.client.Client`
        The Client instance this state is attached to
    config : :class:`StateConfig`
        The configuration for this state instance
    me : :class:`disco.types.user.User`
        The currently logged in user
    dms : dict(snowflake, :class:`disco.types.channel.Channel`)
        Mapping of all known DM Channels
    guilds : dict(snowflake, :class:`disco.types.guild.Guild`)
        Mapping of all known/loaded Guilds
    channels : dict(snowflake, :class:`disco.types.channel.Channel`)
        Weak mapping of all known/loaded Channels
    users : dict(snowflake, :class:`disco.types.user.User`)
        Weak mapping of all known/loaded Users
    voice_states : dict(str, :class:`disco.types.voice.VoiceState`)
        Weak mapping of all known/active Voice States
    messages : Optional[dict(snowflake, :class:`deque`)]
        Mapping of channel ids to deques containing :class:`StackMessage` objects
    """
    EVENTS = [
        'Ready', 'GuildCreate', 'GuildUpdate', 'GuildDelete', 'GuildMemberAdd',
        'GuildMemberRemove', 'GuildMemberUpdate', 'GuildMembersChunk',
        'GuildRoleCreate', 'GuildRoleUpdate', 'GuildRoleDelete',
        'GuildEmojisUpdate', 'ChannelCreate', 'ChannelUpdate', 'ChannelDelete',
        'VoiceStateUpdate', 'MessageCreate', 'PresenceUpdate'
    ]

    def __init__(self, client, config):
        self.client = client
        self.config = config

        self.ready = Event()
        self.guilds_waiting_sync = 0

        self.me = None
        self.dms = HashMap()
        self.guilds = HashMap()
        self.channels = HashMap(weakref.WeakValueDictionary())
        self.users = HashMap(weakref.WeakValueDictionary())
        self.voice_states = HashMap(weakref.WeakValueDictionary())

        # If message tracking is enabled, listen to those events
        if self.config.track_messages:
            self.messages = DefaultHashMap(
                lambda: deque(maxlen=self.config.track_messages_size))
            self.EVENTS += ['MessageDelete', 'MessageDeleteBulk']

        # The bound listener objects
        self.listeners = []
        self.bind()

    def unbind(self):
        """
        Unbinds all bound event listeners for this state object.
        """
        map(lambda k: k.unbind(), self.listeners)
        self.listeners = []

    def bind(self):
        """
        Binds all events for this state object, storing the listeners for later
        unbinding.
        """
        assert not len(
            self.listeners), 'Binding while already bound is dangerous'

        for event in self.EVENTS:
            func = 'on_' + inflection.underscore(event)
            self.listeners.append(
                self.client.events.on(event, getattr(self, func)))

    def fill_messages(self, channel):
        for message in reversed(next(channel.messages_iter(bulk=True))):
            self.messages[channel.id].append(
                StackMessage(message.id, message.channel_id,
                             message.author.id))

    def on_ready(self, event):
        self.me = event.user
        self.guilds_waiting_sync = len(event.guilds)

        for dm in event.private_channels:
            self.dms[dm.id] = dm
            self.channels[dm.id] = dm

    def on_message_create(self, event):
        if self.config.track_messages:
            self.messages[event.message.channel_id].append(
                StackMessage(event.message.id, event.message.channel_id,
                             event.message.author.id))

        if event.message.channel_id in self.channels:
            self.channels[
                event.message.channel_id].last_message_id = event.message.id

    def on_message_delete(self, event):
        if event.channel_id not in self.messages:
            return

        sm = next(
            (i for i in self.messages[event.channel_id] if i.id == event.id),
            None)
        if not sm:
            return

        self.messages[event.channel_id].remove(sm)

    def on_message_delete_bulk(self, event):
        if event.channel_id not in self.messages:
            return

        # TODO: performance
        for sm in list(self.messages[event.channel_id]):
            if sm.id in event.ids:
                self.messages[event.channel_id].remove(sm)

    def on_guild_create(self, event):
        if event.unavailable is False:
            self.guilds_waiting_sync -= 1
            if self.guilds_waiting_sync <= 0:
                self.ready.set()

        self.guilds[event.guild.id] = event.guild
        self.channels.update(event.guild.channels)

        for member in six.itervalues(event.guild.members):
            self.users[member.user.id] = member.user

        for voice_state in six.itervalues(event.guild.voice_states):
            self.voice_states[voice_state.session_id] = voice_state

        if self.config.sync_guild_members:
            event.guild.sync()

    def on_guild_update(self, event):
        self.guilds[event.guild.id].update(event.guild)

    def on_guild_delete(self, event):
        if event.id in self.guilds:
            # Just delete the guild, channel references will fall
            del self.guilds[event.id]

    def on_channel_create(self, event):
        if event.channel.is_guild and event.channel.guild_id in self.guilds:
            self.guilds[event.channel.guild_id].channels[
                event.channel.id] = event.channel
            self.channels[event.channel.id] = event.channel
        elif event.channel.is_dm:
            self.dms[event.channel.id] = event.channel
            self.channels[event.channel.id] = event.channel

    def on_channel_update(self, event):
        if event.channel.id in self.channels:
            self.channels[event.channel.id].update(event.channel)

    def on_channel_delete(self, event):
        if event.channel.is_guild and event.channel.guild and event.channel.id in event.channel.guild.channels:
            del event.channel.guild.channels[event.channel.id]
        elif event.channel.is_dm and event.channel.id in self.dms:
            del self.dms[event.channel.id]

    def on_voice_state_update(self, event):
        # Happy path: we have the voice state and want to update/delete it
        guild = self.guilds.get(event.state.guild_id)
        if not guild:
            return

        if event.state.session_id in guild.voice_states:
            if event.state.channel_id:
                guild.voice_states[event.state.session_id].update(event.state)
            else:
                del guild.voice_states[event.state.session_id]

                # Prevent a weird race where events come in before the guild_create (I think...)
                if event.state.session_id in self.voice_states:
                    del self.voice_states[event.state.session_id]
        elif event.state.channel_id:
            guild.voice_states[event.state.session_id] = event.state
            self.voice_states[event.state.session_id] = event.state

    def on_guild_member_add(self, event):
        if event.member.user.id not in self.users:
            self.users[event.member.user.id] = event.member.user
        else:
            event.member.user = self.users[event.member.user.id]

        if event.member.guild_id not in self.guilds:
            return

        self.guilds[event.member.guild_id].members[
            event.member.id] = event.member

    def on_guild_member_update(self, event):
        if event.member.guild_id not in self.guilds:
            return

        if event.member.id not in self.guilds[event.member.guild_id].members:
            return

        self.guilds[event.member.guild_id].members[event.member.id].update(
            event.member)

    def on_guild_member_remove(self, event):
        if event.guild_id not in self.guilds:
            return

        if event.user.id not in self.guilds[event.guild_id].members:
            return

        del self.guilds[event.guild_id].members[event.user.id]

    def on_guild_members_chunk(self, event):
        if event.guild_id not in self.guilds:
            return

        guild = self.guilds[event.guild_id]
        for member in event.members:
            member.guild_id = guild.id
            guild.members[member.id] = member
            self.users[member.id] = member.user

    def on_guild_role_create(self, event):
        if event.guild_id not in self.guilds:
            return

        self.guilds[event.guild_id].roles[event.role.id] = event.role

    def on_guild_role_update(self, event):
        if event.guild_id not in self.guilds:
            return

        self.guilds[event.guild_id].roles[event.role.id].update(event.role)

    def on_guild_role_delete(self, event):
        if event.guild_id not in self.guilds:
            return

        if event.role_id not in self.guilds[event.guild_id].roles:
            return

        del self.guilds[event.guild_id].roles[event.role_id]

    def on_guild_emojis_update(self, event):
        if event.guild_id not in self.guilds:
            return

        self.guilds[event.guild_id].emojis = HashMap(
            {i.id: i
             for i in event.emojis})

    def on_presence_update(self, event):
        if event.user.id in self.users:
            self.users[event.user.id].update(event.presence.user)
            self.users[event.user.id].presence = event.presence
            event.presence.user = self.users[event.user.id]

        if event.guild_id not in self.guilds:
            return

        if event.user.id not in self.guilds[event.guild_id].members:
            return

        self.guilds[event.guild_id].members[event.user.id].user.update(
            event.user)