예제 #1
0
def test_emitter_sequential():
    emitter = Emitter()

    class TestEvent(object):
        pass

    def handler_contained(event):
        time.sleep(random.randint(1, 100) / 100)
        event.value += 1
        event.order.append(event.value)

        if event.value == 3:
            event.result.set()

    event = TestEvent()
    event.order = []
    event.value = 0
    event.result = AsyncResult()

    emitter.on('test', handler_contained, priority=Priority.SEQUENTIAL)
    emitter.emit('test', event)
    emitter.emit('test', event)
    emitter.emit('test', event)

    event.result.wait()
    assert event.order == [1, 2, 3]
    assert event.value == 3
예제 #2
0
class Websocket(LoggingClass, websocket.WebSocketApp):
    """
    A utility class which wraps the functionality of :class:`websocket.WebSocketApp`
    changing its behavior to better conform with standard style across disco.

    The major difference comes with the move from callback functions, to all
    events being piped into a single emitter.
    """
    def __init__(self, *args, **kwargs):
        LoggingClass.__init__(self)
        websocket.WebSocketApp.__init__(self, *args, **kwargs)

        self.emitter = Emitter(spawn_each=True)

        # Hack to get events to emit
        for var in six.iterkeys(self.__dict__):
            if not var.startswith('on_'):
                continue

            setattr(self, var, var)

    def _get_close_args(self, data):
        if data and len(data) >= 2:
            code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2])
            reason = data[2:].decode('utf-8')
            return [code, reason]
        return [None, None]

    def _callback(self, callback, *args):
        if not callback:
            return

        self.emitter.emit(callback, *args)
예제 #3
0
def test_emitter_before():
    emitter = Emitter()

    class TestEvent(object):
        pass

    def handler_before(event):
        time.sleep(1)
        event.value = 1

    def handler_default(event):
        event.result.set(event.value)

    event = TestEvent()
    event.result = AsyncResult()

    emitter.on('test', handler_before, priority=Priority.BEFORE)
    emitter.on('test', handler_default)
    emitter.emit('test', event)

    assert event.result.wait() == 1
예제 #4
0
class VoiceClient(LoggingClass):
    def __init__(self, channel, encoder=None):
        super(VoiceClient, self).__init__()

        if not channel.is_voice:
            raise ValueError(
                'Cannot spawn a VoiceClient for a non-voice channel')

        self.channel = channel
        self.client = self.channel.client
        self.encoder = encoder or JSONEncoder

        # Bind to some WS packets
        self.packets = Emitter(gevent.spawn)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)

        # State + state change emitter
        self.state = VoiceState.DISCONNECTED
        self.state_emitter = Emitter(gevent.spawn)

        # Connection metadata
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.port = None
        self.secret_box = None
        self.udp = None

        # Voice data state
        self.sequence = 0
        self.timestamp = 0

        self.update_listener = None

        # Websocket connection
        self.ws = None
        self.heartbeat_task = None

    def __repr__(self):
        return u'<VoiceClient {}>'.format(self.channel)

    def set_state(self, state):
        self.log.debug('[%s] state %s -> %s', self, self.state, state)
        prev_state = self.state
        self.state = state
        self.state_emitter.emit(state, prev_state)

    def heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time() * 1000)
            gevent.sleep(interval / 1000)

    def set_speaking(self, value):
        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': 0,
        })

    def send(self, op, data):
        self.log.debug('[%s] sending OP %s (data = %s)', self, op, data)
        self.ws.send(self.encoder.encode({
            'op': op.value,
            'd': data,
        }), self.encoder.OPCODE)

    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Recived Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.port = data['port']

        self.heartbeat_task = gevent.spawn(self.heartbeat,
                                           data['heartbeat_interval'])

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.endpoint, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.endpoint, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': 'xsalsa20_poly1305'
                }
            })

    def on_voice_sdp(self, sdp):
        self.log.info(
            '[%s] Recieved session description, connection completed', self)
        # Create a secret box for encryption/decryption
        self.secret_box = nacl.secret.SecretBox(
            bytes(bytearray(sdp['secret_key'])))

        # Toggle speaking state so clients learn of our SSRC
        self.set_speaking(True)
        self.set_speaking(False)
        gevent.sleep(0.25)

        self.set_state(VoiceState.CONNECTED)

    def on_voice_server_update(self, data):
        if self.channel.guild_id != data.guild_id or not data.token:
            return

        if self.token and self.token != data.token:
            return

        self.log.info('[%s] Recieved VOICE_SERVER_UPDATE (state = %s)', self,
                      self.state)

        self.token = data.token
        self.set_state(VoiceState.AUTHENTICATING)

        self.endpoint = data.endpoint.split(':', 1)[0]
        self.ws = Websocket('wss://' + self.endpoint)
        self.ws.emitter.on('on_open', self.on_open)
        self.ws.emitter.on('on_error', self.on_error)
        self.ws.emitter.on('on_close', self.on_close)
        self.ws.emitter.on('on_message', self.on_message)
        self.ws.run_forever()

    def on_message(self, msg):
        try:
            data = self.encoder.decode(msg)
            self.packets.emit(VoiceOPCode[data['op']], data['d'])
        except:
            self.log.exception('Failed to parse voice gateway message: ')

    def on_error(self, err):
        # TODO: raise an exception here
        self.log.error('[%s] Voice websocket error: %s', self, err)

    def on_open(self):
        self.send(
            VoiceOPCode.IDENTIFY, {
                'server_id': self.channel.guild_id,
                'user_id': self.client.state.me.id,
                'session_id': self.client.gw.session_id,
                'token': self.token
            })

    def on_close(self, code, error):
        self.log.warning('[%s] Voice websocket disconnected (%s, %s)', self,
                         code, error)

        if self.state == VoiceState.CONNECTED:
            self.log.info('Attempting voice reconnection')
            self.connect()

    def connect(self, timeout=5, mute=False, deaf=False):
        self.log.debug('[%s] Attempting connection', self)
        self.set_state(VoiceState.AWAITING_ENDPOINT)

        self.update_listener = self.client.events.on(
            'VoiceServerUpdate', self.on_voice_server_update)

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': mute,
                'self_deaf': deaf,
                'guild_id': int(self.channel.guild_id),
                'channel_id': int(self.channel.id),
            })

        if not self.state_emitter.once(VoiceState.CONNECTED, timeout=timeout):
            raise VoiceException('Failed to connect to voice', self)

    def disconnect(self):
        self.log.debug('[%s] disconnect called', self)
        self.set_state(VoiceState.DISCONNECTED)

        if self.heartbeat_task:
            self.heartbeat_task.kill()
            self.heartbeat_task = None

        if self.ws and self.ws.sock.connected:
            self.ws.close()

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': False,
                'self_deaf': False,
                'guild_id': int(self.channel.guild_id),
                'channel_id': None,
            })

    def send_frame(self, *args, **kwargs):
        self.udp.send_frame(*args, **kwargs)
예제 #5
0
파일: core.py 프로젝트: Eros/speedboat
class CorePlugin(Plugin):
    def load(self, ctx):
        init_db(ENV)

        self.startup = ctx.get('startup', datetime.utcnow())
        self.guilds = ctx.get('guilds', {})

        self.emitter = Emitter(gevent.spawn)

        super(CorePlugin, self).load(ctx)

        # Overwrite the main bot instances plugin loader so we can magicfy events
        self.bot.add_plugin = self.our_add_plugin

        if ENV != 'prod':
            self.spawn(self.wait_for_plugin_changes)

        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)

    def spawn_wait_for_actions(self, *args, **kwargs):
        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)
        self._wait_for_actions_greenlet.link_exception(
            self.spawn_wait_for_actions)

    def our_add_plugin(self, cls, *args, **kwargs):
        if getattr(cls, 'global_plugin', False):
            Bot.add_plugin(self.bot, cls, *args, **kwargs)
            return

        inst = cls(self.bot, None)
        inst.register_trigger('command', 'pre',
                              functools.partial(self.on_pre, inst))
        inst.register_trigger('listener', 'pre',
                              functools.partial(self.on_pre, inst))
        Bot.add_plugin(self.bot, inst, *args, **kwargs)

    def wait_for_plugin_changes(self):
        import gevent_inotifyx as inotify

        fd = inotify.init()
        inotify.add_watch(fd, 'rowboat/plugins/', inotify.IN_MODIFY)
        while True:
            events = inotify.get_events(fd)
            for event in events:
                # Can't reload core.py sadly
                if event.name.startswith('core.py'):
                    continue

                plugin_name = '{}Plugin'.format(
                    event.name.split('.', 1)[0].title())
                plugin = next((v for k, v in self.bot.plugins.items()
                               if k.lower() == plugin_name.lower()), None)
                if plugin:
                    self.log.info('Detected change in %s, reloading...',
                                  plugin_name)
                    try:
                        plugin.reload()
                    except Exception:
                        self.log.exception('Failed to reload: ')

    def wait_for_actions(self):
        ps = rdb.pubsub()
        ps.subscribe('actions')

        for item in ps.listen():
            if item['type'] != 'message':
                continue

            data = json.loads(item['data'])
            if data['type'] == 'GUILD_UPDATE' and data['id'] in self.guilds:
                with self.send_control_message() as embed:
                    embed.title = u'Reloaded config for {}'.format(
                        self.guilds[data['id']].name)

                self.log.info(u'Reloading guild %s',
                              self.guilds[data['id']].name)

                # Refresh config, mostly to validate
                try:
                    config = self.guilds[data['id']].get_config(refresh=True)

                    # Reload the guild entirely
                    self.guilds[data['id']] = Guild.with_id(data['id'])

                    # Update guild access
                    self.update_rowboat_guild_access()

                    # Finally, emit the event
                    self.emitter.emit('GUILD_CONFIG_UPDATE',
                                      self.guilds[data['id']], config)
                except:
                    self.log.exception(u'Failed to reload config for guild %s',
                                       self.guilds[data['id']].name)
                    continue
            elif data['type'] == 'RESTART':
                self.log.info('Restart requested, signaling parent')
                os.kill(os.getppid(), signal.SIGUSR1)

    def unload(self, ctx):
        ctx['guilds'] = self.guilds
        ctx['startup'] = self.startup
        super(CorePlugin, self).unload(ctx)

    def update_rowboat_guild_access(self):
        #        if ROWBOAT_GUILD_ID not in self.state.guilds or ENV != 'prod':
        #            return

        rb_guild = self.state.guilds.get(ROWBOAT_GUILD_ID)
        if not rb_guild:
            return

        self.log.info('Updating rowboat guild access')

        guilds = Guild.select(Guild.guild_id, Guild.config).where(
            (Guild.enabled == 1))

        users_who_should_have_access = set()
        for guild in guilds:
            if 'web' not in guild.config:
                continue

            for user_id in guild.config['web'].keys():
                try:
                    users_who_should_have_access.add(int(user_id))
                except:
                    self.log.warning('Guild %s has invalid user ACLs: %s',
                                     guild.guild_id, guild.config['web'])

        # TODO: sharding
        users_who_have_access = {
            i.id
            for i in rb_guild.members.values()
            if ROWBOAT_USER_ROLE_ID in i.roles
        }

        remove_access = set(users_who_have_access) - set(
            users_who_should_have_access)
        add_access = set(users_who_should_have_access) - set(
            users_who_have_access)

        for user_id in remove_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.remove_role(ROWBOAT_USER_ROLE_ID)

        for user_id in add_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.add_role(ROWBOAT_USER_ROLE_ID)

    def on_pre(self, plugin, func, event, args, kwargs):
        """
        This function handles dynamically dispatching and modifying events based
        on a specific guilds configuration. It is called before any handler of
        either commands or listeners.
        """
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        if guild_id not in self.guilds:
            if isinstance(event, CommandEvent):
                if event.command.metadata.get('global_', False):
                    return event
            elif hasattr(func, 'subscriptions'):
                if func.subscriptions[0].metadata.get('global_', False):
                    return event

            return

        if hasattr(plugin, 'WHITELIST_FLAG'):
            if not int(
                    plugin.WHITELIST_FLAG) in self.guilds[guild_id].whitelist:
                return

        event.base_config = self.guilds[guild_id].get_config()
        if not event.base_config:
            return

        plugin_name = plugin.name.lower().replace('plugin', '')
        if not getattr(event.base_config.plugins, plugin_name, None):
            return

        self._attach_local_event_data(event, plugin_name, guild_id)

        return event

    def get_config(self, guild_id, *args, **kwargs):
        # Externally Used
        return self.guilds[guild_id].get_config(*args, **kwargs)

    def get_guild(self, guild_id):
        # Externally Used
        return self.guilds[guild_id]

    def _attach_local_event_data(self, event, plugin_name, guild_id):
        if not hasattr(event, 'config'):
            event.config = LocalProxy()

        if not hasattr(event, 'rowboat_guild'):
            event.rowboat_guild = LocalProxy()

        event.config.set(getattr(event.base_config.plugins, plugin_name))
        event.rowboat_guild.set(self.guilds[guild_id])

    @Plugin.schedule(290, init=False)
    def update_guild_bans(self):
        to_update = [
            guild
            for guild in Guild.select().where((
                Guild.last_ban_sync < (datetime.utcnow() - timedelta(days=1)))
                                              | (Guild.last_ban_sync >> None))
            if guild.guild_id in self.client.state.guilds
        ]

        # Update 10 at a time
        for guild in to_update[:10]:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

    @Plugin.listen('GuildUpdate')
    def on_guild_update(self, event):
        self.log.info('Got guild update for guild %s (%s)', event.guild.id,
                      event.guild.channels)

    @Plugin.listen('GuildBanAdd')
    def on_guild_ban_add(self, event):
        GuildBan.ensure(self.client.state.guilds.get(event.guild_id),
                        event.user)

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        GuildBan.delete().where((GuildBan.user_id == event.user.id)
                                & (GuildBan.guild_id == event.guild_id))

    @contextlib.contextmanager
    def send_control_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Rowboat {}'.format('Production' if ENV ==
                                                  'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_CONTROL_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send control message:')
            return

    @Plugin.listen('Resumed')
    def on_resumed(self, event):
        Notification.dispatch(
            Notification.Types.RESUME,
            trace=event.trace,
            env=ENV,
        )

        with self.send_control_message() as embed:
            embed.title = 'Resumed'
            embed.color = 0xffb347
            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)
            embed.add_field(name='Replayed Events',
                            value=str(self.client.gw.replayed_events))

    @Plugin.listen('Ready', priority=Priority.BEFORE)
    def on_ready(self, event):
        reconnects = self.client.gw.reconnects
        self.log.info('Started session %s', event.session_id)
        Notification.dispatch(
            Notification.Types.CONNECT,
            trace=event.trace,
            env=ENV,
        )

        with self.send_control_message() as embed:
            if reconnects:
                embed.title = 'Reconnected'
                embed.color = 0xffb347
            else:
                embed.title = 'Connected'
                embed.color = 0x77dd77

            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)

    @Plugin.listen('GuildCreate',
                   priority=Priority.BEFORE,
                   conditional=lambda e: not e.created)
    def on_guild_create(self, event):
        try:
            guild = Guild.with_id(event.id)
        except Guild.DoesNotExist:
            # If the guild is not awaiting setup, leave it now
            if not rdb.sismember(GUILDS_WAITING_SETUP_KEY, str(
                    event.id)) and event.id != ROWBOAT_GUILD_ID:
                self.log.warning(
                    'Leaving guild %s (%s), not within setup list', event.id,
                    event.name)
                event.guild.leave()
            return

        if not guild.enabled:
            return

        config = guild.get_config()
        if not config:
            return

        # Ensure we're updated
        self.log.info('Syncing Guild and Members in: %s (%s)',
                      event.guild.name, event.guild.id)
        guild.sync(event.guild)
        event.guild.sync()

        self.guilds[event.id] = guild

        if config.nickname:

            def set_nickname():
                m = event.members.select_one(id=self.state.me.id)
                if m and m.nick != config.nickname:
                    try:
                        m.set_nickname(config.nickname)
                    except APIException as e:
                        self.log.warning(
                            'Failed to set nickname for guild %s (%s)',
                            event.guild, e.content)

            self.spawn_later(5, set_nickname)

    def get_level(self, guild, user):
        config = (guild.id in self.guilds
                  and self.guilds.get(guild.id).get_config())

        user_level = 0
        if config:
            member = guild.get_member(user)
            if not member:
                return user_level

            for oid in member.roles:
                if oid in config.levels and config.levels[oid] > user_level:
                    user_level = config.levels[oid]

            # User ID overrides should override all others
            if member.id in config.levels:
                user_level = config.levels[member.id]

        return user_level

    @Plugin.listen('MessageCreate')
    def on_message_create(self, event):
        """
        This monstrosity of a function handles the parsing and dispatching of
        commands.
        """
        # Ignore messages sent by bots
        if event.message.author.bot:
            return

        # If this is message for a guild, grab the guild object
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        guild = self.guilds.get(event.guild.id) if guild_id else None
        config = guild and guild.get_config()

        # If the guild has configuration, use that (otherwise use defaults)
        if config and config.commands:
            commands = list(
                self.bot.get_commands_for_message(config.commands.mention, {},
                                                  config.commands.prefix,
                                                  event.message))
        elif guild_id:
            # Otherwise, default to requiring mentions
            commands = list(
                self.bot.get_commands_for_message(True, {}, '', event.message))
        else:
            if ENV != 'prod':
                if not event.message.content.startswith(ENV + '!'):
                    return
                event.message.content = event.message.content[len(ENV) + 1:]

            # DM's just use the commands (no prefix/mention)
            commands = list(
                self.bot.get_commands_for_message(False, {}, '',
                                                  event.message))

        # If we didn't find any matching commands, return
        if not len(commands):
            return

        event.user_level = self.get_level(event.guild,
                                          event.author) if event.guild else 0

        # Grab whether this user is a global admin
        # TODO: cache this
        global_admin = rdb.sismember('global_admins', event.author.id)

        # Iterate over commands and find a match
        for command, match in commands:
            if command.level == -1 and not global_admin:
                continue

            level = command.level

            if guild and not config and command.triggers[0] != 'setup':
                continue
            elif config and config.commands and command.plugin != self:
                overrides = {}
                for obj in config.commands.get_command_override(command):
                    overrides.update(obj)

                if overrides.get('disabled'):
                    continue

                level = overrides.get('level', level)

            if not global_admin and event.user_level < level:
                continue

            with timed('rowboat.command.duration',
                       tags={
                           'plugin': command.plugin.name,
                           'command': command.name
                       }):
                try:
                    command_event = CommandEvent(command, event.message, match)
                    command_event.user_level = event.user_level
                    command.plugin.execute(command_event)
                except CommandResponse as e:
                    event.reply(e.response)
                except:
                    tracked = Command.track(event, command, exception=True)
                    self.log.exception('Command error:')

                    with self.send_control_message() as embed:
                        embed.title = u'Command Error: {}'.format(command.name)
                        embed.color = 0xff6961
                        embed.add_field(name='Author',
                                        value='({}) `{}`'.format(
                                            event.author, event.author.id),
                                        inline=True)
                        embed.add_field(name='Channel',
                                        value='({}) `{}`'.format(
                                            event.channel.name,
                                            event.channel.id),
                                        inline=True)
                        embed.description = '```{}```'.format(u'\n'.join(
                            tracked.traceback.split('\n')[-8:]))

                    return event.reply(
                        '<:{}> something went wrong, perhaps try again later'.
                        format(RED_TICK_EMOJI))

            Command.track(event, command)

            # Dispatch the command used modlog event
            if config:
                modlog_config = getattr(config.plugins, 'modlog', None)
                if not modlog_config:
                    return

                self._attach_local_event_data(event, 'modlog', event.guild.id)

                plugin = self.bot.plugins.get('ModLogPlugin')
                if plugin:
                    plugin.log_action(Actions.COMMAND_USED, event)

            return

    @Plugin.command('setup')
    def command_setup(self, event):
        if not event.guild:
            return event.msg.reply(
                ':warning: this command can only be used in servers')

        # Make sure we're not already setup
        if event.guild.id in self.guilds:
            return event.msg.reply(':warning: this server is already setup')

        global_admin = rdb.sismember('global_admins', event.author.id)

        # Make sure this is the owner of the server
        if not global_admin:
            if not event.guild.owner_id == event.author.id:
                return event.msg.reply(
                    ':warning: only the server owner can setup rowboat')

        # Make sure we have admin perms
        m = event.guild.members.select_one(id=self.state.me.id)
        if not m.permissions.administrator and not global_admin:
            return event.msg.reply(
                ':warning: bot must have the Administrator permission')

        guild = Guild.setup(event.guild)
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(event.guild.id))
        self.guilds[event.guild.id] = guild
        event.msg.reply(':ok_hand: successfully loaded configuration')

    @Plugin.command('nuke', '<user:snowflake> <reason:str...>', level=-1)
    def nuke(self, event, user, reason):
        contents = []

        for gid, guild in self.guilds.items():
            guild = self.state.guilds[gid]
            perms = guild.get_permissions(self.state.me)

            if not perms.ban_members and not perms.administrator:
                contents.append(u':x: {} - No Permissions'.format(guild.name))
                continue

            try:
                Infraction.ban(self.bot.plugins.get('AdminPlugin'),
                               event,
                               user,
                               reason,
                               guild=guild)
            except:
                contents.append(u':x: {} - Unknown Error'.format(guild.name))
                self.log.exception('Failed to force ban %s in %s', user, gid)

            contents.append(
                u':white_check_mark: {} - :regional_indicator_f:'.format(
                    guild.name))

        event.msg.reply('Results:\n' + '\n'.join(contents))

    @Plugin.command('about')
    def command_about(self, event):
        embed = MessageEmbed()
        embed.set_author(name='Rowboat',
                         icon_url=self.client.state.me.avatar_url,
                         url='https://rowboat.party/')
        embed.description = BOT_INFO
        embed.add_field(name='Servers',
                        value=str(Guild.select().count()),
                        inline=True)
        embed.add_field(name='Uptime',
                        value=humanize.naturaldelta(datetime.utcnow() -
                                                    self.startup),
                        inline=True)
        event.msg.reply(embed=embed)

    @Plugin.command('uptime', level=-1)
    def command_uptime(self, event):
        event.msg.reply('Rowboat was started {}'.format(
            humanize.naturaldelta(datetime.utcnow() - self.startup)))

    @Plugin.command('source', '<command>', level=-1)
    def command_source(self, event, command=None):
        for cmd in self.bot.commands:
            if command.lower() in cmd.triggers:
                break
        else:
            event.msg.reply(u"Couldn't find command for `{}`".format(
                S(command, escape_codeblocks=True)))
            return

        code = cmd.func.__code__
        lines, firstlineno = inspect.getsourcelines(code)

        event.msg.reply(
            '<https://github.com/b1naryth1ef/rowboat/blob/master/{}#L{}-{}>'.
            format(code.co_filename, firstlineno, firstlineno + len(lines)))

    @Plugin.command('eval', level=-1)
    def command_eval(self, event):
        ctx = {
            'bot': self.bot,
            'client': self.bot.client,
            'state': self.bot.client.state,
            'event': event,
            'msg': event.msg,
            'guild': event.msg.guild,
            'channel': event.msg.channel,
            'author': event.msg.author
        }

        # Mulitline eval
        src = event.codeblock
        if src.count('\n'):
            lines = filter(bool, src.split('\n'))
            if lines[-1] and 'return' not in lines[-1]:
                lines[-1] = 'return ' + lines[-1]
            lines = '\n'.join('    ' + i for i in lines)
            code = 'def f():\n{}\nx = f()'.format(lines)
            local = {}

            try:
                exec compile(code, '<eval>', 'exec') in ctx, local
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

            result = pprint.pformat(local['x'])
        else:
            try:
                result = str(eval(src, ctx))
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

        if len(result) > 1990:
            event.msg.reply('', attachments=[('result.txt', result)])
        else:
            event.msg.reply(PY_CODE_BLOCK.format(result))

    @Plugin.command('sync-bans', group='control', level=-1)
    def control_sync_bans(self, event):
        guilds = list(Guild.select().where(Guild.enabled == 1))

        msg = event.msg.reply(':timer: pls wait while I sync...')

        for guild in guilds:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

        msg.edit('<:{}> synced {} guilds'.format(GREEN_TICK_EMOJI,
                                                 len(guilds)))

    @Plugin.command('reconnect', group='control', level=-1)
    def control_reconnect(self, event):
        event.msg.reply('Ok, closing connection')
        self.client.gw.ws.close()

    @Plugin.command('invite', '<guild:snowflake>', group='guilds', level=-1)
    def guild_join(self, event, guild):
        guild = self.state.guilds.get(guild)
        if not guild:
            return event.msg.reply(
                ':no_entry_sign: invalid or unknown guild ID')

        msg = event.msg.reply(
            u'Ok, hold on while I get you setup with an invite link to {}'.
            format(guild.name, ))

        general_channel = guild.channels[guild.id]

        try:
            invite = general_channel.create_invite(
                max_age=300,
                max_uses=1,
                unique=True,
            )
        except:
            return msg.edit(
                u':no_entry_sign: Hmmm, something went wrong creating an invite for {}'
                .format(guild.name, ))

        msg.edit(u'Ok, here is a temporary invite for you: {}'.format(
            invite.code, ))

    @Plugin.command('wh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_whitelist(self, event, guild):
        rdb.sadd(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply('Ok, guild %s is now in the whitelist' % guild)

    @Plugin.command('unwh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_unwhitelist(self, event, guild):
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply(
            'Ok, I\'ve made sure guild %s is no longer in the whitelist' %
            guild)

    @Plugin.command('disable', '<plugin:str>', group='plugins', level=-1)
    def plugin_disable(self, event, plugin):
        plugin = self.bot.plugins.get(plugin)
        if not plugin:
            return event.msg.reply(
                'Hmmm, it appears that plugin doesn\'t exist!?')
        self.bot.rmv_plugin(plugin.__class__)
        event.msg.reply('Ok, that plugin has been disabled and unloaded')

    @Plugin.command('commands', group='control', level=-1)
    def control_commands(self, event):
        event.msg.reply(
            '__**Punishments**__\n`!mute <mention or ID> [reason]` - Mutes user from talking in text channel (role must be set up).\n`!unmute <mention or ID> [reason]` - Unmutes user.\n`!tempmute <mention or ID> <duration> [reason]` - Temporarily mutes user from talking in text channel for duration.\n`!kick <mention or ID> [reason]` - Kicks user from server.\n`!ban <mention or ID> [reason]` - Bans user, does not delete messages. Must still be in server.\n`!unban <ID> [reason]` - Unbans user. Must use ID.\n`!tempban <mention or ID> <duration> [reason]` - Temporarily bans user for duration.\n`!forceban <ID> <reason>` - Bans user who is not in the server. Must use ID.\n`!softban <mention or ID> [reason]` - Softbans (bans/unbans) user.\n\n__**Admin Utilities**__\n`!clean all <#>` - Deletes # of messages in current channel.\n`!clean bots <#>` - Deletes # of messages sent by bots in current channel.\n`!clean user <mention or ID> <#>` - Deletes # of user\'s messages in current channel. Must use ID if they are no longer in the server.\n`!archive user <mention or ID> [#]` - Creates an archive with all messages found by user.\n`!archive (here / all ) [#]` - Creates an archive with all messages in the server.\n`!archive channel <channel> [#]` - Creates an archive with all messages in the channel.\n`!search <tag or ID>` - Returns some information about the user, ID, time joined, infractions, etc.\n`!role add <mention or ID> <role> [reason]` - Adds the role (either ID or fuzzy-match name) to the user.\n`!role rmv/remove <mention or ID> <role> [reason]` - Removes the role from the user.\n`!r add <duration> <message>` - Adds a reminder to be sent after the specified duration.'
        )
        event.msg.reply(
            '__**Infractions**__\n`!inf search <mention or ID>` - Searches infractions based on the given query.\n`!inf info <inf #>` - Returns information on an infraction.\n`!inf duration <inf #> <duration>` - Updates the duration of an infraction ([temp]ban, [temp]mutes). Duration starts from time of initial action.\n`!reason <inf #> <reason>` - Sets the reason for an infraction.\n\n__**Reactions and Starboard**__\n`!stars <lock | unlock>` - Lock or unlocks the starboard. Locking prevents new posts from being starred.\n`!stars <block | unblock> <mention or ID>` - Blocks or unblocks a user\'s stars from starboard. Their reactions won\'t count and messages won\'t be posted.\n`!stars hide <message ID>` - Removes a starred message from starboard using message ID.\n`!reactions clean <user> [count] [emoji]` - Removes reactions placed by specified user.'
        )
예제 #6
0
파일: client.py 프로젝트: TheaQueen/disco
class VoiceClient(LoggingClass):
    VOICE_GATEWAY_VERSION = 3

    SUPPORTED_MODES = {
        'xsalsa20_poly1305_lite',
        'xsalsa20_poly1305_suffix',
        'xsalsa20_poly1305',
    }

    def __init__(self, channel, encoder=None, max_reconnects=5):
        super(VoiceClient, self).__init__()

        if not channel.is_voice:
            raise ValueError(
                'Cannot spawn a VoiceClient for a non-voice channel')

        self.channel = channel
        self.client = self.channel.client
        self.encoder = encoder or JSONEncoder
        self.max_reconnects = max_reconnects

        # Bind to some WS packets
        self.packets = Emitter()
        self.packets.on(VoiceOPCode.HELLO, self.on_voice_hello)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.RESUMED, self.on_voice_resumed)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)

        # State + state change emitter
        self.state = VoiceState.DISCONNECTED
        self.state_emitter = Emitter()

        # Connection metadata
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.port = None
        self.mode = None
        self.udp = None

        # Websocket connection
        self.ws = None

        self._session_id = None
        self._reconnects = 0
        self._update_listener = None
        self._heartbeat_task = None

    def __repr__(self):
        return u'<VoiceClient {}>'.format(self.channel)

    def set_state(self, state):
        self.log.debug('[%s] state %s -> %s', self, self.state, state)
        prev_state = self.state
        self.state = state
        self.state_emitter.emit(state, prev_state)

    def _connect_and_run(self):
        self.ws = Websocket('wss://' + self.endpoint +
                            '/v={}'.format(self.VOICE_GATEWAY_VERSION))
        self.ws.emitter.on('on_open', self.on_open)
        self.ws.emitter.on('on_error', self.on_error)
        self.ws.emitter.on('on_close', self.on_close)
        self.ws.emitter.on('on_message', self.on_message)
        self.ws.run_forever()

    def _heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time())
            gevent.sleep(interval / 1000)

    def set_speaking(self, value):
        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': 0,
        })

    def send(self, op, data):
        self.log.debug('[%s] sending OP %s (data = %s)', self, op, data)
        self.ws.send(self.encoder.encode({
            'op': op.value,
            'd': data,
        }), self.encoder.OPCODE)

    def on_voice_hello(self, data):
        self.log.info(
            '[%s] Recieved Voice HELLO payload, starting heartbeater', self)
        self._heartbeat_task = gevent.spawn(self._heartbeat,
                                            data['heartbeat_interval'] * 0.75)
        self.set_state(VoiceState.AUTHENTICATED)

    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Recived Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.port = data['port']

        for mode in self.SUPPORTED_MODES:
            if mode in data['modes']:
                self.mode = mode
                self.log.debug('[%s] Selected mode %s', self, mode)
                break
        else:
            raise Exception('Failed to find a supported voice mode')

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.endpoint, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.endpoint, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': self.mode,
                },
            })

    def on_voice_resumed(self, data):
        self.log.info('[%s] Recieved resumed', self)
        self.set_state(VoiceState.CONNECTED)

    def on_voice_sdp(self, sdp):
        self.log.info(
            '[%s] Recieved session description, connection completed', self)

        # Create a secret box for encryption/decryption
        self.udp.setup_encryption(bytes(bytearray(sdp['secret_key'])))

        # Toggle speaking state so clients learn of our SSRC
        self.set_speaking(True)
        self.set_speaking(False)
        gevent.sleep(0.25)

        self.set_state(VoiceState.CONNECTED)

    def on_voice_server_update(self, data):
        if self.channel.guild_id != data.guild_id or not data.token:
            return

        if self.token and self.token != data.token:
            return

        self.log.info(
            '[%s] Recieved VOICE_SERVER_UPDATE (state = %s / endpoint = %s)',
            self, self.state, data.endpoint)

        self.token = data.token
        self.set_state(VoiceState.AUTHENTICATING)

        self.endpoint = data.endpoint.split(':', 1)[0]

        self._connect_and_run()

    def on_message(self, msg):
        try:
            data = self.encoder.decode(msg)
            self.packets.emit(VoiceOPCode[data['op']], data['d'])
        except Exception:
            self.log.exception('Failed to parse voice gateway message: ')

    def on_error(self, err):
        self.log.error('[%s] Voice websocket error: %s', self, err)

    def on_open(self):
        if self._session_id:
            return self.send(
                VoiceOPCode.RESUME, {
                    'server_id': self.channel.guild_id,
                    'user_id': self.client.state.me.id,
                    'session_id': self._session_id,
                    'token': self.token,
                })

        self._session_id = self.client.gw.session_id

        self.send(
            VoiceOPCode.IDENTIFY, {
                'server_id': self.channel.guild_id,
                'user_id': self.client.state.me.id,
                'session_id': self._session_id,
                'token': self.token,
            })

    def on_close(self, code, reason):
        self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self,
                         code, reason, self._reconnects)

        if self._heartbeat_task:
            self._heartbeat_task.kill()

        # If we're not in a connected state, don't try to resume/reconnect
        if self.state != VoiceState.CONNECTED:
            return

        self.log.info('[%s] Attempting Websocket Resumption', self)
        self._reconnects += 1

        if self.max_reconnects and self._reconnects > self.max_reconnects:
            raise VoiceException(
                'Failed to reconnect after {} attempts, giving up'.format(
                    self.max_reconnects))

        self.set_state(VoiceState.RECONNECTING)

        # Don't resume for these error codes:
        if code and 4000 <= code <= 4016:
            self._session_id = None

            if self.udp and self.udp.connected:
                self.udp.disconnect()

        wait_time = (self._reconnects - 1) * 5
        self.log.info('[%s] Will attempt to %s after %s seconds', self,
                      'resume' if self._session_id else 'reconnect', wait_time)
        gevent.sleep(wait_time)

        self._connect_and_run()

    def connect(self, timeout=5, mute=False, deaf=False):
        self.log.debug('[%s] Attempting connection', self)
        self.set_state(VoiceState.AWAITING_ENDPOINT)

        self._update_listener = self.client.events.on(
            'VoiceServerUpdate', self.on_voice_server_update)

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': mute,
                'self_deaf': deaf,
                'guild_id': int(self.channel.guild_id),
                'channel_id': int(self.channel.id),
            })

        if not self.state_emitter.once(VoiceState.CONNECTED, timeout=timeout):
            self.disconnect()
            raise VoiceException('Failed to connect to voice', self)

    def disconnect(self):
        self.log.debug('[%s] disconnect called', self)
        self.set_state(VoiceState.DISCONNECTED)

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': False,
                'self_deaf': False,
                'guild_id': int(self.channel.guild_id),
                'channel_id': None,
            })

    def send_frame(self, *args, **kwargs):
        self.udp.send_frame(*args, **kwargs)

    def increment_timestamp(self, *args, **kwargs):
        self.udp.increment_timestamp(*args, **kwargs)
예제 #7
0
파일: player.py 프로젝트: samuelotti/disco
class Player(object):
    Events = Enum(
        'START_PLAY',
        'STOP_PLAY',
        'PAUSE_PLAY',
        'RESUME_PLAY',
        'DISCONNECT',
    )

    def __init__(self, client, queue=None):
        self.client = client

        # Queue contains playable items
        self.queue = queue or PlayableQueue()

        # Whether we're playing music (true for lifetime)
        self.playing = True

        # Set to an event when playback is paused
        self.paused = None

        # Current playing item
        self.now_playing = None

        # Current play task
        self.play_task = None

        # Core task
        self.run_task = gevent.spawn(self.run)

        # Event triggered when playback is complete
        self.complete = gevent.event.Event()

        # Event emitter for metadata
        self.events = Emitter(spawn_each=True)

    def disconnect(self):
        self.client.disconnect()
        self.events.emit(self.Events.DISCONNECT)

    def skip(self):
        self.play_task.kill()

    def pause(self):
        if self.paused:
            return
        self.paused = gevent.event.Event()
        self.events.emit(self.Events.PAUSE_PLAY)

    def resume(self):
        if self.paused:
            self.paused.set()
            self.paused = None
            self.events.emit(self.Events.RESUME_PLAY)

    def play(self, item):
        # Grab the first frame before we start anything else, sometimes playables
        #  can do some lengthy async tasks here to setup the playable and we
        #  don't want that lerp the first N frames of the playable into playing
        #  faster
        frame = item.next_frame()
        if frame is None:
            return

        start = time.time()
        loops = 0

        while True:
            loops += 1

            if self.paused:
                self.client.set_speaking(False)
                self.paused.wait()
                gevent.sleep(2)
                self.client.set_speaking(True)
                start = time.time()
                loops = 0

            if self.client.state == VoiceState.DISCONNECTED:
                return

            if self.client.state != VoiceState.CONNECTED:
                self.client.state_emitter.wait(VoiceState.CONNECTED)

            self.client.send_frame(frame)
            self.client.timestamp += item.samples_per_frame
            if self.client.timestamp > MAX_TIMESTAMP:
                self.client.timestamp = 0

            frame = item.next_frame()
            if frame is None:
                return

            next_time = start + 0.02 * loops
            delay = max(0, 0.02 + (next_time - time.time()))
            gevent.sleep(delay)

    def run(self):
        self.client.set_speaking(True)

        while self.playing:
            self.now_playing = self.queue.get()

            self.events.emit(self.Events.START_PLAY, self.now_playing)
            self.play_task = gevent.spawn(self.play, self.now_playing)
            self.play_task.join()
            self.events.emit(self.Events.STOP_PLAY, self.now_playing)

            if self.client.state == VoiceState.DISCONNECTED:
                self.playing = False
                self.complete.set()
                return

        self.client.set_speaking(False)
        self.disconnect()
예제 #8
0
class Player(LoggingClass):
    Events = Enum(
        'START_PLAY',
        'STOP_PLAY',
        'PAUSE_PLAY',
        'RESUME_PLAY',
        'DISCONNECT',
    )

    def __init__(self, client, queue=None):
        super(Player, self).__init__()
        self.client = client

        self.last_activity = time.time()

        self.force_kick = False

        self.already_play = False

        self.force_return = 1
        self.max_returns = 1
        self.sleep_time_returns = 3

        # replay
        self.replay = False

        # Text channel
        self.text_id = None

        # Queue contains playable items
        self.queue = queue or PlayableQueue()

        # Whether we're playing music (true for lifetime)
        self.playing = True

        # Set to an event when playback is paused
        self.paused = None

        # Current playing item
        self.now_playing = None

        # Last playing item
        self.then_playing = None

        # Current play task
        self.play_task = None

        # Core task
        self.run_task = gevent.spawn(self.run)

        # Event triggered when playback is complete
        self.complete = gevent.event.Event()

        # Event emitter for metadata
        self.events = Emitter()

    def disconnect(self):
        self.client.disconnect()
        self.events.emit(self.Events.DISCONNECT)

    def skip(self):
        if self.now_playing and self.now_playing.source:
            self.now_playing.source.killed()
        else:
            print u'source have unknown type???'

    def pause(self):
        if self.paused:
            return
        self.paused = gevent.event.Event()
        self.events.emit(self.Events.PAUSE_PLAY)

    def resume(self):
        if self.paused:
            self.paused.set()
            self.paused = None
            self.events.emit(self.Events.RESUME_PLAY)

    def play(self, item):
        # Grab the first frame before we start anything else, sometimes playables
        #  can do some lengthy async tasks here to setup the playable and we
        #  don't want that lerp the first N frames of the playable into playing
        #  faster
        frame = item.next_frame()
        if frame is None:
            return

        connected = True

        start = time.time()
        loops = 0

        if getattr(item.source, "broadcast", True) and getattr(
                item.source, "embed", None):
            try:
                gevent.spawn(self.client.client.api.channels_messages_create,
                             channel=self.text_id,
                             embed=item.source.embed)
            except Exception as error:
                print error
                pass

        try:
            print item
        except Exception as error:
            print error
            pass

        while True:
            loops += 1

            if self.client.state == VoiceState.DISCONNECTED:
                return

            if self.client.state != VoiceState.CONNECTED:
                self.client.state_emitter.once(VoiceState.CONNECTED,
                                               timeout=30)

            # Send the voice frame and increment our timestamp
            try:
                self.client.send_frame(frame)
                self.client.increment_timestamp(item.samples_per_frame)
                self.client.set_speaking(True)
            except WebSocketConnectionClosedException as error:
                connected = False
                print "WS Error: {}, gid: {}".format(
                    error, self.client.channel.guild_id)
                self.client.set_state(VoiceState.RECONNECTING)
                while self.force_return and self.max_returns < 15 and not connected:  # and item.source.proc_working:
                    print "gid: {}, try number: {}, connect to WebSocket".format(
                        self.client.channel.guild_id, self.max_returns)
                    self.max_returns += 1
                    try:
                        #self.client.connect()
                        self.client.state_emitter.once(VoiceState.CONNECTED,
                                                       timeout=5)
                        self.max_returns = 0
                        connected = True
                    except Exception as error:
                        print "gid: {}, connect error: {}, sleep... {} sec".format(
                            self.client.channel.guild_id, error,
                            self.sleep_time_returns)
                        gevent.sleep(self.sleep_time_returns)
                        pass

                if not self.max_returns < 15:
                    self.client.set_state(VoiceState.DISCONNECTED)
                    return

# Check proc live
            if not item.source.proc_working:
                return

# Get next
            frame = item.next_frame()
            self.last_activity = time.time()
            if frame is None:
                return

            next_time = start + 0.02 * loops
            delay = max(0, 0.02 + (next_time - time.time()))
            item.source.played += delay
            gevent.sleep(delay)

    def run(self):

        while self.playing:
            self.now_playing = self.queue.get()

            self.events.emit(self.Events.START_PLAY, self.now_playing)
            self.play_task = gevent.spawn(self.play, self.now_playing)

            self.already_play = True
            self.last_activity = time.time()

            self.play_task.join()
            self.events.emit(self.Events.STOP_PLAY, self.now_playing)

            self.now_playing = None
            self.already_play = False

            if self.client.state == VoiceState.DISCONNECTED:
                self.playing = False
                self.queue.clear()
                self.complete.set()

        self.client.set_speaking(False)
        self.disconnect()
예제 #9
0
파일: core.py 프로젝트: OGNova/airplane
class CorePlugin(Plugin):
    def load(self, ctx):
        init_db(ENV)

        self.startup = ctx.get('startup', datetime.utcnow())
        self.guilds = ctx.get('guilds', {})

        self.emitter = Emitter(gevent.spawn)

        super(CorePlugin, self).load(ctx)

        # Overwrite the main bot instances plugin loader so we can magicfy events
        self.bot.add_plugin = self.our_add_plugin

        self.spawn(self.wait_for_plugin_changes)

        self.global_config = None

        with open('config.yaml', 'r') as f:
            self.global_config = load(f)

        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)

    def spawn_wait_for_actions(self, *args, **kwargs):
        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)
        self._wait_for_actions_greenlet.link_exception(
            self.spawn_wait_for_actions)

    def our_add_plugin(self, cls, *args, **kwargs):
        if getattr(cls, 'global_plugin', False):
            Bot.add_plugin(self.bot, cls, *args, **kwargs)
            return

        inst = cls(self.bot, None)
        inst.register_trigger('command', 'pre',
                              functools.partial(self.on_pre, inst))
        inst.register_trigger('listener', 'pre',
                              functools.partial(self.on_pre, inst))
        Bot.add_plugin(self.bot, inst, *args, **kwargs)

    def wait_for_plugin_changes(self):
        import gevent_inotifyx as inotify

        fd = inotify.init()
        inotify.add_watch(fd, 'rowboat/plugins/', inotify.IN_MODIFY)
        while True:
            events = inotify.get_events(fd)
            for event in events:
                # Can't reload core.py sadly
                if event.name.startswith('core.py'):
                    continue

                plugin_name = '{}Plugin'.format(
                    event.name.split('.', 1)[0].title())
                plugin = next((v for k, v in self.bot.plugins.items()
                               if k.lower() == plugin_name.lower()), None)
                if plugin:
                    self.log.info('Detected change in %s, reloading...',
                                  plugin_name)
                    try:
                        plugin.reload()
                    except Exception:
                        self.log.exception('Failed to reload: ')

    def wait_for_actions(self):
        ps = rdb.pubsub()
        ps.subscribe('actions')

        for item in ps.listen():
            if item['type'] != 'message':
                continue

            data = json.loads(item['data'])
            if data['type'] == 'GUILD_UPDATE' and data['id'] in self.guilds:
                with self.send_config_message() as embed:
                    embed.title = u'Reloaded config for {}'.format(
                        self.guilds[data['id']].name)

                self.log.info(u'Reloading guild %s',
                              self.guilds[data['id']].name)

                # Refresh config, mostly to validate
                try:
                    config = self.guilds[data['id']].get_config(refresh=True)

                    # Reload the guild entirely
                    self.guilds[data['id']] = Guild.with_id(data['id'])

                    # Update guild access
                    self.update_rowboat_guild_access()

                    # Finally, emit the event
                    self.emitter.emit('GUILD_CONFIG_UPDATE',
                                      self.guilds[data['id']], config)
                except:
                    self.log.exception(u'Failed to reload config for guild %s',
                                       self.guilds[data['id']].name)
                    continue
            elif data['type'] == 'RESTART':
                self.log.info('Restart requested, signaling parent')
                os.kill(os.getppid(), signal.SIGUSR1)
            elif data['type'] == 'GUILD_DELETE' and data['id'] in self.guilds:
                with self.send_config_message() as embed:
                    embed.color = 0xff6961
                    embed.title = u'Guild Force Deleted {}'.format(
                        self.guilds[data['id']].name, )

                self.log.info(u'Leaving guild %s',
                              self.guilds[data['id']].name)
                self.guilds[data['id']].leave()

    def unload(self, ctx):
        ctx['guilds'] = self.guilds
        ctx['startup'] = self.startup
        super(CorePlugin, self).unload(ctx)

    def update_rowboat_guild_access(self):
        if ROWBOAT_GUILD_ID not in self.state.guilds or ENV != 'prod':
            return

        rb_guild = self.state.guilds.get(ROWBOAT_GUILD_ID)
        if not rb_guild:
            return

        self.log.info('Updating Airplane guild access')

        guilds = Guild.select(Guild.guild_id, Guild.config).where(
            (Guild.enabled == 1))

        users_who_should_have_access = set()
        for guild in guilds:
            if 'web' not in guild.config:
                continue

            for user_id in guild.config['web'].keys():
                try:
                    users_who_should_have_access.add(int(user_id))
                except:
                    self.log.warning('Guild %s has invalid user ACLs: %s',
                                     guild.guild_id, guild.config['web'])

        # TODO: sharding
        users_who_have_access = {
            i.id
            for i in rb_guild.members.values()
            if ROWBOAT_USER_ROLE_ID in i.roles
        }

        remove_access = set(users_who_have_access) - set(
            users_who_should_have_access)
        add_access = set(users_who_should_have_access) - set(
            users_who_have_access)

        for user_id in remove_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.remove_role(ROWBOAT_USER_ROLE_ID)

        for user_id in add_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.add_role(ROWBOAT_USER_ROLE_ID)

    def on_pre(self, plugin, func, event, args, kwargs):
        """
        This function handles dynamically dispatching and modifying events based
        on a specific guilds configuration. It is called before any handler of
        either commands or listeners.
        """
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        if guild_id not in self.guilds:
            if isinstance(event, CommandEvent):
                if event.command.metadata.get('global_', False):
                    return event
            elif hasattr(func, 'subscriptions'):
                if func.subscriptions[0].metadata.get('global_', False):
                    return event

            return

        if hasattr(plugin, 'WHITELIST_FLAG'):
            if not int(
                    plugin.WHITELIST_FLAG) in self.guilds[guild_id].whitelist:
                return

        event.base_config = self.guilds[guild_id].get_config()
        if not event.base_config:
            return

        plugin_name = plugin.name.lower().replace('plugin', '')
        if not getattr(event.base_config.plugins, plugin_name, None):
            return

        self._attach_local_event_data(event, plugin_name, guild_id)

        return event

    def get_config(self, guild_id, *args, **kwargs):
        # Externally Used
        return self.guilds[guild_id].get_config(*args, **kwargs)

    def get_guild(self, guild_id):
        # Externally Used
        return self.guilds[guild_id]

    def _attach_local_event_data(self, event, plugin_name, guild_id):
        if not hasattr(event, 'config'):
            event.config = LocalProxy()

        if not hasattr(event, 'rowboat_guild'):
            event.rowboat_guild = LocalProxy()

        event.config.set(getattr(event.base_config.plugins, plugin_name))
        event.rowboat_guild.set(self.guilds[guild_id])

    @Plugin.schedule(290, init=False)
    def update_guild_bans(self):
        to_update = [
            guild
            for guild in Guild.select().where((
                Guild.last_ban_sync < (datetime.utcnow() - timedelta(days=1)))
                                              | (Guild.last_ban_sync >> None))
            if guild.guild_id in self.client.state.guilds
        ]

        # Update 10 at a time
        for guild in to_update[:10]:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

    @Plugin.listen('GuildUpdate')
    def on_guild_update(self, event):
        self.log.info('Got guild update for guild %s (%s)', event.guild.id,
                      event.guild.channels)

    # @Plugin.listen('GuildMembersChunk')
    # def on_guild_members_chunk(self, event):
    #     self.log.info('Got members chunk for guild %s', event.guild_id)

    @Plugin.listen('GuildBanAdd')
    def on_guild_ban_add(self, event):
        GuildBan.ensure(self.client.state.guilds.get(event.guild_id),
                        event.user)

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        GuildBan.delete().where((GuildBan.user_id == event.user.id)
                                & (GuildBan.guild_id == event.guild_id))

    @contextlib.contextmanager
    def send_launch_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Airplane {}'.format('Production' if ENV ==
                                                   'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_LAUNCH_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send control message:')
            return

    @contextlib.contextmanager
    def send_config_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Airplane {}'.format('Production' if ENV ==
                                                   'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_CONFIG_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send spam control message:')
            return

    @contextlib.contextmanager
    def send_error_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Airplane {}'.format('Production' if ENV ==
                                                   'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(ROWBOAT_ERROR_CHANNEL,
                                                         embed=embed)
        except:
            self.log.exception('Failed to send spam control message:')
            return

    @Plugin.listen('Resumed')
    def on_resumed(self, event):
        Notification.dispatch(
            Notification.Types.RESUME,
            trace=event.trace,
            env=ENV,
        )

        with self.send_launch_message() as embed:
            embed.title = 'Resumed'
            embed.color = 0xffb347
            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)
            embed.add_field(name='Replayed Events',
                            value=str(self.client.gw.replayed_events))

    @Plugin.listen('Ready', priority=Priority.BEFORE)
    def on_ready(self, event):
        reconnects = self.client.gw.reconnects
        self.log.info('Started session %s', event.session_id)
        Notification.dispatch(
            Notification.Types.CONNECT,
            trace=event.trace,
            env=ENV,
        )

        with self.send_launch_message() as embed:
            if reconnects:
                embed.title = 'Reconnected'
                embed.color = 0xffb347
            else:
                embed.title = 'Connected'
                embed.color = 0x77dd77

            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)

    @Plugin.listen('GuildCreate',
                   priority=Priority.BEFORE,
                   conditional=lambda e: not e.created)
    def on_guild_create(self, event):
        try:
            guild = Guild.with_id(event.id)
        except Guild.DoesNotExist:
            # If the guild is not awaiting setup, leave it now
            if not rdb.sismember(GUILDS_WAITING_SETUP_KEY, str(
                    event.id)) and event.id != ROWBOAT_GUILD_ID:
                self.log.warning(
                    'Leaving guild %s (%s), not within setup list', event.id,
                    event.name)
                event.guild.leave()
            return

        if not guild.enabled:
            return

        config = guild.get_config()
        if not config:
            return

        # Ensure we're updated
        self.log.info('Syncing guild %s', event.guild.id)
        guild.sync(event.guild)

        self.guilds[event.id] = guild

        if config.nickname:

            def set_nickname():
                m = event.members.select_one(id=self.state.me.id)
                if m and m.nick != config.nickname:
                    try:
                        m.set_nickname(config.nickname)
                    except APIException as e:
                        self.log.warning(
                            'Failed to set nickname for guild %s (%s)',
                            event.guild, e.content)

            self.spawn_later(5, set_nickname)

    def get_level(self, guild, user):
        config = (guild.id in self.guilds
                  and self.guilds.get(guild.id).get_config())

        user_level = 0
        if config:
            member = guild.get_member(user)
            if not member:
                return user_level

            for oid in member.roles:
                if oid in config.levels and config.levels[oid] > user_level:
                    user_level = config.levels[oid]

            # User ID overrides should override all others
            if member.id in config.levels:
                user_level = config.levels[member.id]

        return user_level

    @Plugin.listen('MessageCreate')
    def on_message_create(self, event):
        """
        This monstrosity of a function handles the parsing and dispatching of
        commands.
        """
        # Ignore messages sent by bots
        if event.message.author.bot:
            return

        if rdb.sismember('ignored_channels', event.message.channel_id):
            return

        if event.message.content.startswith('gh/'):
            event.message.reply('https://github.com/{}/{}'.format(
                event.message.content.split('/')[1],
                event.message.content.split('/')[2]))
            return

        # If this is message for a guild, grab the guild object
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        guild = self.guilds.get(event.guild.id) if guild_id else None
        config = guild and guild.get_config()

        # If the guild has configuration, use that (otherwise use defaults)
        if config and config.commands:
            commands = list(
                self.bot.get_commands_for_message(config.commands.mention, {},
                                                  config.commands.prefix,
                                                  event.message))
        elif guild_id:
            # Otherwise, default to requiring mentions
            commands = list(
                self.bot.get_commands_for_message(True, {}, '', event.message))
        else:
            if ENV != 'prod':
                if not event.message.content.startswith(ENV + '!'):
                    return
                event.message.content = event.message.content[len(ENV) + 1:]

            # DM's just use the commands (no prefix/mention)
            commands = list(
                self.bot.get_commands_for_message(False, {}, '',
                                                  event.message))

        # If we didn't find any matching commands, return
        if not len(commands):
            return

        event.user_level = self.get_level(event.guild,
                                          event.author) if event.guild else 0

        # Grab whether this user is a global admin
        # TODO: cache this
        global_admin = rdb.sismember('global_admins', event.author.id)

        # Iterate over commands and find a match
        for command, match in commands:
            if command.level == -1 and not global_admin:
                continue

            level = command.level

            if guild and not config and command.triggers[0] != 'setup':
                continue
            elif config and config.commands and command.plugin != self:
                overrides = {}
                for obj in config.commands.get_command_override(command):
                    overrides.update(obj)

                if overrides.get('disabled'):
                    continue

                level = overrides.get('level', level)

            if not global_admin and event.user_level < level:
                continue

            with timed('rowboat.command.duration',
                       tags={
                           'plugin': command.plugin.name,
                           'command': command.name
                       }):
                try:
                    command_event = CommandEvent(command, event.message, match)
                    command_event.user_level = event.user_level
                    command.plugin.execute(command_event)
                except CommandResponse as e:
                    event.reply(e.response)
                except:
                    tracked = Command.track(event, command, exception=True)
                    self.log.exception('Command error:')

                    with self.send_error_message() as embed:
                        embed.title = u'Command Error: {}'.format(command.name)
                        embed.color = 0xff6961
                        embed.add_field(name='Server',
                                        value='({}) `{}`'.format(
                                            event.guild.name, event.guild.id),
                                        inline=False)
                        embed.add_field(name='Exact Command Ran',
                                        value='`{}`'.format(event.content))
                        embed.add_field(name='Author',
                                        value='(`{}`) `{}`'.format(
                                            unicode(
                                                event.author).encode('utf-8'),
                                            event.author.id),
                                        inline=True)
                        embed.add_field(name='Channel',
                                        value='({}) `{}`'.format(
                                            event.channel.name,
                                            event.channel.id),
                                        inline=True)
                        embed.description = '```{}```'.format(u'\n'.join(
                            tracked.traceback.split('\n')[-8:]))

                    return event.reply(
                        '<:{}> something went wrong, perhaps try again later'.
                        format(RED_TICK_EMOJI))

            Command.track(event, command)

            # Dispatch the command used modlog event
            if config:
                modlog_config = getattr(config.plugins, 'modlog', None)
                if not modlog_config:
                    return

                self._attach_local_event_data(event, 'modlog', event.guild.id)

                plugin = self.bot.plugins.get('ModLogPlugin')
                if plugin:
                    plugin.log_action(Actions.COMMAND_USED, event)

            return

    @Plugin.command('setup')
    def command_setup(self, event):
        if not event.guild:
            return event.msg.reply(
                ':warning: this command can only be used in servers')

        # Make sure we're not already setup
        if event.guild.id in self.guilds:
            return event.msg.reply(':warning: this server is already setup')

        global_admin = rdb.sismember('global_admins', event.author.id)

        # Make sure this is the owner of the server
        if not global_admin:
            if not event.guild.owner_id == event.author.id:
                return event.msg.reply(
                    ':warning: only the server owner can setup Airplane')

        # Make sure we have admin perms
        m = event.guild.members.select_one(id=self.state.me.id)
        if not m.permissions.administrator and not global_admin:
            return event.msg.reply(
                ':warning: bot must have the Administrator permission')

        guild = Guild.setup(event.guild)
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(event.guild.id))
        self.guilds[event.guild.id] = guild
        event.msg.reply(':ok_hand: successfully loaded configuration')

    @Plugin.command('about')
    def command_about(self, event):
        embed = MessageEmbed()
        embed.set_author(name='Airplane',
                         icon_url=self.client.state.me.avatar_url,
                         url='https://dash.airplane.gg/')
        embed.description = BOT_INFO
        embed.add_field(name='Servers',
                        value=str(Guild.select().count()),
                        inline=True)
        embed.add_field(name='Uptime',
                        value=humanize.naturaldelta(datetime.utcnow() -
                                                    self.startup),
                        inline=True)
        event.msg.reply(embed=embed)

    @Plugin.command('uptime', level=-1)
    def command_uptime(self, event):
        event.msg.reply('Airplane has been up for {}'.format(
            humanize.naturaldelta(datetime.utcnow() - self.startup)))

    @Plugin.command('source', '<command>', level=-1)
    def command_source(self, event, command=None):
        for cmd in self.bot.commands:
            if command.lower() in cmd.triggers:
                break
        else:
            event.msg.reply(u"Couldn't find command for `{}`".format(
                S(command, escape_codeblocks=True)))
            return

        code = cmd.func.__code__
        lines, firstlineno = inspect.getsourcelines(code)

        event.msg.reply(
            '<https://github.com/OGNova/airplane/blob/master/{}#L{}-{}>'.
            format(code.co_filename, firstlineno, firstlineno + len(lines)))

    @Plugin.command('eval', level=-1)
    def command_eval(self, event):
        ctx = {
            'bot': self.bot,
            'client': self.bot.client,
            'state': self.bot.client.state,
            'event': event,
            'msg': event.msg,
            'guild': event.msg.guild,
            'channel': event.msg.channel,
            'author': event.msg.author
        }

        # Mulitline eval
        src = event.codeblock
        if src.count('\n'):
            lines = filter(bool, src.split('\n'))
            if lines[-1] and 'return' not in lines[-1]:
                lines[-1] = 'return ' + lines[-1]
            lines = '\n'.join('    ' + i for i in lines)
            code = 'def f():\n{}\nx = f()'.format(lines)
            local = {}

            try:
                exec compile(code, '<eval>', 'exec') in ctx, local
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

            result = pprint.pformat(local['x'])
        else:
            try:
                result = str(eval(src, ctx))
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

        if len(result) > 1990:
            event.msg.reply('', attachments=[('result.txt', result)])
        else:
            event.msg.reply(PY_CODE_BLOCK.format(result))

    @Plugin.command('sync-bans', group='control', level=-1)
    def control_sync_bans(self, event):
        guilds = list(Guild.select().where(Guild.enabled == 1))

        msg = event.msg.reply(':timer: pls wait while I sync...')

        for guild in guilds:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

        msg.edit('<:{}> synced {} guilds'.format(GREEN_TICK_EMOJI,
                                                 len(guilds)))

    @Plugin.command('reconnect', group='control', level=-1)
    def control_reconnect(self, event):
        event.msg.reply('Ok, closing connection')
        self.client.gw.ws.close()

    @Plugin.command('invite', '<guild:snowflake>', group='guilds', level=-1)
    def guild_join(self, event, guild):
        guild = self.state.guilds.get(guild)
        if not guild:
            return event.msg.reply(
                '<:deny:470285164313051138> invalid or unknown guild ID')

        msg = event.msg.reply(
            u'Ok, hold on while I get you setup with an invite link to {}'.
            format(guild.name, ))

        general_channel = guild.channels[guild.id]

        try:
            invite = general_channel.create_invite(
                max_age=300,
                max_uses=1,
                unique=True,
            )
        except:
            return msg.edit(
                u'<:deny:470285164313051138> Hmmm, something went wrong creating an invite for {}'
                .format(guild.name, ))

        msg.edit(u'Ok, here is a temporary invite for you: {}'.format(
            invite.code, ))

    @Plugin.command('locate', '<user:user|snowflake>', level=-1)
    def command_locate(self, event, user):
        if isinstance(user, (int, long)):
            uid = user
            user = self.state.users.get(uid)
            if not user:
                return event.msg.reply('User {} not found.'.format(uid))

        buff = ''
        count = 0
        guilds = self.state.guilds.values()
        guilds = sorted(guilds, key=lambda g: g.name)
        for guild in guilds:
            member = guild.members.get(user.id)
            if not member:
                continue
            guild = S(u'{} - {} (level: {})\n'.format(
                guild.id, guild.name, self.get_level(guild, user.id)),
                      escape_codeblocks=True)
            if len(guild) + len(buff) > 1920:
                event.msg.reply(u'```{}```'.format(buff))
                buff = ''
            buff += guild
            count += 1

        user = u'{} ({} - {})'.format(user, user.mention, user.id)

        if not count:
            return event.msg.reply(u'User {} not found.'.format(user))

        event.msg.reply(u'```{}```*User {} found in {} server{}.*'.format(
            buff, user, count, 's' if count > 1 else ''))

    @Plugin.command('wh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_whitelist(self, event, guild):
        rdb.sadd(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply('Ok, guild %s is now in the whitelist' % guild)

    @Plugin.command('unwh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_unwhitelist(self, event, guild):
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply(
            'Ok, I\'ve made sure guild %s is no longer in the whitelist' %
            guild)

    @Plugin.command('disable', '<plugin:str>', group='plugins', level=-1)
    def plugin_disable(self, event, plugin):
        plugin = self.bot.plugins.get(plugin)
        if not plugin:
            return event.msg.reply(
                'Hmmm, it appears that plugin doesn\'t exist!?')
        self.bot.rmv_plugin(plugin.__class__)
        event.msg.reply('Ok, that plugin has been disabled and unloaded')

    def default_color(self, avatar_color):
        switcher = {
            'blurple': "https://cdn.discordapp.com/embed/avatars/0.png",
            'grey': "https://cdn.discordapp.com/embed/avatars/1.png",
            'green': "https://cdn.discordapp.com/embed/avatars/2.png",
            'orange': "https://cdn.discordapp.com/embed/avatars/3.png",
            'red': "https://cdn.discordapp.com/embed/avatars/4.png"
        }
        return switcher.get(avatar_color)

    @Plugin.listen('MessageCreate')
    def dm_listener(self, event):
        from rowboat.util.images import get_dominant_colors_user
        global_admin = rdb.sismember('global_admins', event.author.id)
        if global_admin or event.author.id == 351097525928853506:
            return
        if event.guild == None:
            MODIFIER_GRAVE_ACCENT = u'\u02CB'
            msg = event
            message_content = msg.content.replace('`', MODIFIER_GRAVE_ACCENT)
            author_id = msg.author.id
            discrim = str(msg.author.discriminator)
            avatar_name = msg.author.avatar
            content = []
            embed = MessageEmbed()

            if not avatar_name:
                avatar = default_color(str(msg.author.default_avatar))
            elif avatar_name.startswith('a_'):
                avatar = u'https://cdn.discordapp.com/avatars/{}/{}.gif'.format(
                    author_id, avatar_name)
            else:
                avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(
                    author_id, avatar_name)
            embed.set_author(name='{} ({})'.format(msg.author, author_id),
                             icon_url=avatar)
            embed.set_thumbnail(url=avatar)

            content.append(u'**\u276F New DM:**')
            content.append(u'Content: ```{}```'.format(message_content))
            embed.description = '\n'.join(content)
            embed.timestamp = datetime.utcnow().isoformat()
            try:
                embed.color = get_dominant_colors_user(msg.author, avatar)
            except:
                embed.color = '00000000'
            self.bot.client.api.channels_messages_create('540020613272829996',
                                                         '',
                                                         embed=embed)
예제 #10
0
class VoiceClient(LoggingClass):
    def __init__(self, channel, encoder=None):
        super(VoiceClient, self).__init__()

        assert channel.is_voice, 'Cannot spawn a VoiceClient for a non-voice channel'
        self.channel = channel
        self.client = self.channel.client
        self.encoder = encoder or JSONEncoder

        self.packets = Emitter(gevent.spawn)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)

        # State
        self.state = VoiceState.DISCONNECTED
        self.connected = gevent.event.Event()
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.port = None

        self.update_listener = None

        # Websocket connection
        self.ws = None
        self.heartbeat_task = None

    def heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time() * 1000)
            gevent.sleep(interval / 1000)

    def set_speaking(self, value):
        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': 0,
        })

    def send(self, op, data):
        self.ws.send(self.encoder.encode({
            'op': op.value,
            'd': data,
        }), self.encoder.OPCODE)

    def on_voice_ready(self, data):
        self.state = VoiceState.CONNECTING
        self.ssrc = data['ssrc']
        self.port = data['port']

        self.heartbeat_task = gevent.spawn(self.heartbeat,
                                           data['heartbeat_interval'])

        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.endpoint, self.port)

        if not ip:
            self.disconnect()
            return

        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': 'plain'
                }
            })

    def on_voice_sdp(self, data):
        # Toggle speaking state so clients learn of our SSRC
        self.set_speaking(True)
        self.set_speaking(False)
        gevent.sleep(0.25)

        self.state = VoiceState.CONNECTED
        self.connected.set()

    def on_voice_server_update(self, data):
        if self.channel.guild_id != data.guild_id or not data.token:
            return

        if self.token and self.token != data.token:
            return

        self.token = data.token
        self.state = VoiceState.AUTHENTICATING

        self.endpoint = data.endpoint.split(':', 1)[0]
        self.ws = Websocket(
            'wss://' + self.endpoint,
            on_message=self.on_message,
            on_error=self.on_error,
            on_open=self.on_open,
            on_close=self.on_close,
        )
        self.ws.run_forever()

    def on_message(self, ws, msg):
        try:
            data = self.encoder.decode(msg)
        except:
            self.log.exception('Failed to parse voice gateway message: ')

        self.packets.emit(VoiceOPCode[data['op']], data['d'])

    def on_error(self, ws, err):
        # TODO
        self.log.warning('Voice websocket error: {}'.format(err))

    def on_open(self, ws):
        self.send(
            VoiceOPCode.IDENTIFY, {
                'server_id': self.channel.guild_id,
                'user_id': self.client.state.me.id,
                'session_id': self.client.gw.session_id,
                'token': self.token
            })

    def on_close(self, ws, code, error):
        # TODO
        self.log.warning('Voice websocket disconnected (%s, %s)', code, error)

    def connect(self, timeout=5, mute=False, deaf=False):
        self.state = VoiceState.AWAITING_ENDPOINT

        self.update_listener = self.client.events.on(
            'VoiceServerUpdate', self.on_voice_server_update)

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': mute,
                'self_deaf': deaf,
                'guild_id': int(self.channel.guild_id),
                'channel_id': int(self.channel.id),
            })

        if not self.connected.wait(
                timeout) or self.state != VoiceState.CONNECTED:
            raise VoiceException('Failed to connect to voice', self)

    def disconnect(self):
        self.state = VoiceState.DISCONNECTED

        if self.heartbeat_task:
            self.heartbeat_task.kill()
            self.heartbeat_task = None

        if self.ws and self.ws.sock.connected:
            self.ws.close()

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': False,
                'self_deaf': False,
                'guild_id': int(self.channel.guild_id),
                'channel_id': None,
            })
예제 #11
0
파일: core.py 프로젝트: Lymia/rowboat
class CorePlugin(Plugin):
    def load(self, ctx):
        init_db(ENV)

        self.startup = ctx.get('startup', datetime.utcnow())
        self.guilds = ctx.get('guilds', {})

        self.emitter = Emitter(gevent.spawn)

        super(CorePlugin, self).load(ctx)

        # Overwrite the main bot instances plugin loader so we can magicfy events
        self.bot.add_plugin = self.our_add_plugin

        if ENV != 'prod':
            self.spawn(self.wait_for_plugin_changes)

        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)

    def spawn_wait_for_actions(self, *args, **kwargs):
        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)
        self._wait_for_actions_greenlet.link_exception(
            self.spawn_wait_for_actions)

    def our_add_plugin(self, cls, *args, **kwargs):
        if getattr(cls, 'global_plugin', False):
            Bot.add_plugin(self.bot, cls, *args, **kwargs)
            return

        inst = cls(self.bot, None)
        inst.register_trigger('command', 'pre',
                              functools.partial(self.on_pre, inst))
        inst.register_trigger('listener', 'pre',
                              functools.partial(self.on_pre, inst))
        Bot.add_plugin(self.bot, inst, *args, **kwargs)

    def wait_for_plugin_changes(self):
        import gevent_inotifyx as inotify

        fd = inotify.init()
        inotify.add_watch(fd, 'rowboat/plugins/', inotify.IN_MODIFY)
        while True:
            events = inotify.get_events(fd)
            for event in events:
                # Can't reload core.py sadly
                if event.name.startswith('core.py'):
                    continue

                plugin_name = '{}Plugin'.format(
                    event.name.split('.', 1)[0].title())
                plugin = next((v for k, v in self.bot.plugins.items()
                               if k.lower() == plugin_name.lower()), None)
                if plugin:
                    self.log.info('Detected change in %s, reloading...',
                                  plugin_name)
                    try:
                        plugin.reload()
                    except Exception:
                        self.log.exception('Failed to reload: ')

    def wait_for_actions(self):
        ps = rdb.pubsub()
        ps.subscribe('actions')

        for item in ps.listen():
            if item['type'] != 'message':
                continue

            data = json.loads(item['data'])
            if data['type'] == 'GUILD_UPDATE' and data['id'] in self.guilds:
                with self.send_control_message() as embed:
                    embed.title = u'Reloaded config for {}'.format(
                        self.guilds[data['id']].name)

                self.log.info(u'Reloading guild %s',
                              self.guilds[data['id']].name)

                # Refresh config, mostly to validate
                try:
                    config = self.guilds[data['id']].get_config(refresh=True)

                    # Reload the guild entirely
                    self.guilds[data['id']] = Guild.with_id(data['id'])

                    # Update guild access
                    self.update_rowboat_guild_access()

                    # Finally, emit the event
                    self.emitter.emit('GUILD_CONFIG_UPDATE',
                                      self.guilds[data['id']], config)
                except:
                    self.log.exception(u'Failed to reload config for guild %s',
                                       self.guilds[data['id']].name)
                    continue
            elif data['type'] == 'RESTART':
                self.log.info('Restart requested, signaling parent')
                os.kill(os.getppid(), signal.SIGUSR1)
            elif data['type'] == 'GUILD_DELETE' and data['id'] in self.guilds:
                with self.send_control_message() as embed:
                    embed.color = 0xff6961
                    embed.title = u'Guild Force Deleted {}'.format(
                        self.guilds[data['id']].name, )

                self.log.info(u'Leaving guild %s',
                              self.guilds[data['id']].name)
                self.guilds[data['id']].leave()

    def unload(self, ctx):
        ctx['guilds'] = self.guilds
        ctx['startup'] = self.startup
        super(CorePlugin, self).unload(ctx)

    def update_rowboat_guild_access(self):
        if ROWBOAT_GUILD_ID not in self.state.guilds or (ENV != 'prod'
                                                         and ENV != 'docker'):
            return

        rb_guild = self.state.guilds.get(ROWBOAT_GUILD_ID)
        if not rb_guild:
            return

        self.log.info('Updating rowboat guild access')

        guilds = Guild.select(Guild.guild_id, Guild.config).where(
            (Guild.enabled == 1))

        users_who_should_have_access = set()
        for guild in guilds:
            if 'web' not in guild.config:
                continue

            for user_id in guild.config['web'].keys():
                try:
                    users_who_should_have_access.add(int(user_id))
                except:
                    self.log.warning('Guild %s has invalid user ACLs: %s',
                                     guild.guild_id, guild.config['web'])

        # TODO: sharding
        users_who_have_access = {
            i.id
            for i in rb_guild.members.values()
            if ROWBOAT_USER_ROLE_ID in i.roles
        }

        remove_access = set(users_who_have_access) - set(
            users_who_should_have_access)
        add_access = set(users_who_should_have_access) - set(
            users_who_have_access)

        for user_id in remove_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.remove_role(ROWBOAT_USER_ROLE_ID)

        for user_id in add_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.add_role(ROWBOAT_USER_ROLE_ID)

    def on_pre(self, plugin, func, event, args, kwargs):
        """
        This function handles dynamically dispatching and modifying events based
        on a specific guilds configuration. It is called before any handler of
        either commands or listeners.
        """
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        if guild_id not in self.guilds:
            if isinstance(event, CommandEvent):
                if event.command.metadata.get('global_', False):
                    return event
            elif hasattr(func, 'subscriptions'):
                if func.subscriptions[0].metadata.get('global_', False):
                    return event

            return

        if hasattr(plugin, 'WHITELIST_FLAG'):
            if not int(
                    plugin.WHITELIST_FLAG) in self.guilds[guild_id].whitelist:
                return

        event.base_config = self.guilds[guild_id].get_config()
        if not event.base_config:
            return

        plugin_name = plugin.name.lower().replace('plugin', '')
        if not getattr(event.base_config.plugins, plugin_name, None):
            return

        self._attach_local_event_data(event, plugin_name, guild_id)

        return event

    def get_config(self, guild_id, *args, **kwargs):
        # Externally Used
        return self.guilds[guild_id].get_config(*args, **kwargs)

    def get_guild(self, guild_id):
        # Externally Used
        return self.guilds[guild_id]

    def _attach_local_event_data(self, event, plugin_name, guild_id):
        if not hasattr(event, 'config'):
            event.config = LocalProxy()

        if not hasattr(event, 'rowboat_guild'):
            event.rowboat_guild = LocalProxy()

        event.config.set(getattr(event.base_config.plugins, plugin_name))
        event.rowboat_guild.set(self.guilds[guild_id])

    @Plugin.schedule(290, init=False)
    def update_guild_bans(self):
        to_update = [
            guild
            for guild in Guild.select().where((
                Guild.last_ban_sync < (datetime.utcnow() - timedelta(days=1)))
                                              | (Guild.last_ban_sync >> None))
            if guild.guild_id in self.client.state.guilds
        ]

        # Update 10 at a time
        for guild in to_update[:10]:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

    @Plugin.listen('GuildUpdate')
    def on_guild_update(self, event):
        self.log.info('Got guild update for guild %s (%s)', event.guild.id,
                      event.guild.channels)

    @Plugin.listen('GuildBanAdd')
    def on_guild_ban_add(self, event):
        GuildBan.ensure(self.client.state.guilds.get(event.guild_id),
                        event.user)

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        GuildBan.delete().where((GuildBan.user_id == event.user.id)
                                & (GuildBan.guild_id == event.guild_id))

    @contextlib.contextmanager
    def send_control_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='RoPolice {}'.format(
            'Production' if ENV == 'prod' or ENV == 'docker' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_CONTROL_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send control message:')
            return

    @Plugin.listen('Resumed')
    def on_resumed(self, event):
        with self.send_control_message() as embed:
            embed.title = 'Resumed'
            embed.color = 0xffb347
            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)
            embed.add_field(name='Replayed Events',
                            value=str(self.client.gw.replayed_events))

    @Plugin.listen('Ready', priority=Priority.BEFORE)
    def on_ready(self, event):
        reconnects = self.client.gw.reconnects
        self.log.info('Started session %s', event.session_id)
        with self.send_control_message() as embed:
            if reconnects:
                embed.title = 'Reconnected'
                embed.color = 0xffb347
            else:
                embed.title = 'Connected'
                embed.color = 0x77dd77

            embed.add_field(name='Gateway Server',
                            value=event.trace[0],
                            inline=False)
            embed.add_field(name='Session Server',
                            value=event.trace[1],
                            inline=False)

    @Plugin.listen('GuildCreate',
                   priority=Priority.BEFORE,
                   conditional=lambda e: not e.created)
    def on_guild_create(self, event):
        try:
            guild = Guild.with_id(event.id)
        except Guild.DoesNotExist:
            # If the guild is not awaiting setup, leave it now
            if not rdb.sismember(GUILDS_WAITING_SETUP_KEY, str(
                    event.id)) and event.id != ROWBOAT_GUILD_ID:
                self.log.warning(
                    'Leaving guild %s (%s), not within setup list', event.id,
                    event.name)
                event.guild.leave()
            return

        if not guild.enabled:
            return

        config = guild.get_config()
        if not config:
            return

        # Ensure we're updated
        self.log.info('Syncing guild %s', event.guild.id)
        guild.sync(event.guild)

        self.guilds[event.id] = guild

        if config.nickname:

            def set_nickname():
                m = event.members.select_one(id=self.state.me.id)
                if m and m.nick != config.nickname:
                    try:
                        m.set_nickname(config.nickname)
                    except APIException as e:
                        self.log.warning(
                            'Failed to set nickname for guild %s (%s)',
                            event.guild, e.content)

            self.spawn_later(5, set_nickname)

    def get_level(self, guild, user):
        config = (guild.id in self.guilds
                  and self.guilds.get(guild.id).get_config())

        user_level = 0
        if config:
            member = guild.get_member(user)
            if not member:
                return user_level

            for oid in member.roles:
                if oid in config.levels and config.levels[oid] > user_level:
                    user_level = config.levels[oid]

            # User ID overrides should override all others
            if member.id in config.levels:
                user_level = config.levels[member.id]

        return user_level

    @Plugin.listen('MessageCreate')
    def on_message_create(self, event):
        """
        This monstrosity of a function handles the parsing and dispatching of
        commands.
        """
        # Ignore messages sent by bots
        #if event.message.author.bot:
        #    return

        # If this is message for a guild, grab the guild object
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        guild = self.guilds.get(event.guild.id) if guild_id else None
        config = guild and guild.get_config()

        # If the guild has configuration, use that (otherwise use defaults)
        if config and config.commands:
            commands = list(
                self.bot.get_commands_for_message(config.commands.mention, {},
                                                  config.commands.prefix,
                                                  event.message))
        elif guild_id:
            # Otherwise, default to requiring mentions
            commands = list(
                self.bot.get_commands_for_message(True, {}, '', event.message))
        else:
            if ENV != 'prod':
                if not event.message.content.startswith(ENV + '!'):
                    return
                event.message.content = event.message.content[len(ENV) + 1:]

            # DM's just use the commands (no prefix/mention)
            commands = list(
                self.bot.get_commands_for_message(False, {}, '',
                                                  event.message))

        # If we didn't find any matching commands, return
        if not len(commands):
            return

        event.user_level = self.get_level(event.guild,
                                          event.author) if event.guild else 0

        # Grab whether this user is a global admin
        # TODO: cache this
        global_admin = rdb.sismember('global_admins', event.author.id)

        # Iterate over commands and find a match
        for command, match in commands:
            if command.level == -1 and not global_admin:
                continue

            level = command.level

            if guild and not config and command.triggers[0] != 'setup':
                continue
            elif config and config.commands and command.plugin != self:
                overrides = {}
                for obj in config.commands.get_command_override(command):
                    overrides.update(obj)

                if overrides.get('disabled'):
                    continue

                level = overrides.get('level', level)

            if not global_admin and event.user_level < level:
                continue

            with timed('rowboat.command.duration',
                       tags={
                           'plugin': command.plugin.name,
                           'command': command.name
                       }):
                try:
                    command_event = CommandEvent(command, event.message, match)
                    command_event.user_level = event.user_level
                    command.plugin.execute(command_event)
                except CommandResponse as e:
                    event.reply(e.response)
                except:
                    tracked = Command.track(event, command, exception=True)
                    self.log.exception('Command error:')

                    with self.send_control_message() as embed:
                        embed.title = u'Command Error: {}'.format(command.name)
                        embed.color = 0xff6961
                        embed.add_field(name='Author',
                                        value='({}) `{}`'.format(
                                            event.author, event.author.id),
                                        inline=True)
                        embed.add_field(name='Channel',
                                        value='({}) `{}`'.format(
                                            event.channel.name,
                                            event.channel.id),
                                        inline=True)
                        embed.description = '```{}```'.format(u'\n'.join(
                            tracked.traceback.split('\n')[-8:]))

                    return event.reply(
                        '<:{}> something went wrong, perhaps try again later'.
                        format(RED_TICK_EMOJI))

            # Dispatch the command used modlog event
            if config:
                modlog_config = getattr(config.plugins, 'modlog', None)
                if not modlog_config:
                    return

                self._attach_local_event_data(event, 'modlog', event.guild.id)

                plugin = self.bot.plugins.get('ModLogPlugin')
                if plugin:
                    plugin.log_action(Actions.COMMAND_USED, event)

            return

    @Plugin.command('setup')
    def command_setup(self, event):
        if not event.guild:
            return event.msg.reply(
                ':warning: this command can only be used in servers')

        # Make sure we're not already setup
        if event.guild.id in self.guilds:
            return event.msg.reply(':warning: this server is already setup')

        global_admin = rdb.sismember('global_admins', event.author.id)

        # Make sure this is the owner of the server
        if not global_admin:
            if not event.guild.owner_id == event.author.id:
                return event.msg.reply(
                    ':warning: only the server owner can setup ropolice')

        # Make sure we have admin perms
        m = event.guild.members.select_one(id=self.state.me.id)
        if not m.permissions.administrator and not global_admin:
            return event.msg.reply(
                ':warning: bot must have the Administrator permission')

        guild = Guild.setup(event.guild)
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(event.guild.id))
        self.guilds[event.guild.id] = guild
        event.msg.reply(':ok_hand: successfully loaded configuration')

    @Plugin.command('about')
    def command_about(self, event):
        embed = MessageEmbed()
        embed.set_author(name='RoPolice',
                         icon_url=self.client.state.me.avatar_url,
                         url='https://www.youtube.com/watch?v=5wFDWP5JwSM')
        embed.description = BOT_INFO
        embed.add_field(name='Servers',
                        value=str(Guild.select().count()),
                        inline=True)
        embed.add_field(name='Uptime',
                        value=humanize.naturaldelta(datetime.utcnow() -
                                                    self.startup),
                        inline=True)
        event.msg.reply(embed=embed)

    @Plugin.command('uptime', level=-1)
    def command_uptime(self, event):
        event.msg.reply('RoPolice was started {} ago'.format(
            humanize.naturaldelta(datetime.utcnow() - self.startup)))

    @Plugin.command('source', '<command>', level=-1)
    def command_source(self, event, command=None):
        for cmd in self.bot.commands:
            if command.lower() in cmd.triggers:
                break
        else:
            event.msg.reply(u"Couldn't find command for `{}`".format(
                S(command, escape_codeblocks=True)))
            return

        code = cmd.func.__code__
        lines, firstlineno = inspect.getsourcelines(code)

        event.msg.reply(
            '<https://github.com/Lymia/rowboat/blob/master/{}#L{}-L{}>'.format(
                code.co_filename, firstlineno, firstlineno + len(lines)))

    @Plugin.command('sync-bans', group='control', level=-1)
    def control_sync_bans(self, event):
        guilds = list(Guild.select().where(Guild.enabled == 1))

        msg = event.msg.reply(':timer: pls wait while I sync...')

        for guild in guilds:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

        msg.edit('<:{}> synced {} guilds'.format(GREEN_TICK_EMOJI,
                                                 len(guilds)))

    @Plugin.command('reconnect', group='control', level=-1)
    def control_reconnect(self, event):
        event.msg.reply('Ok, closing connection')
        self.client.gw.ws.close()

    @Plugin.command('invite', '<guild:snowflake>', group='guilds', level=-1)
    def guild_join(self, event, guild):
        guild = self.state.guilds.get(guild)
        if not guild:
            return event.msg.reply(
                ':no_entry_sign: invalid or unknown guild ID')

        msg = event.msg.reply(
            u'Ok, hold on while I get you setup with an invite link to {}'.
            format(guild.name, ))

        general_channel = guild.channels.itervalues().next()

        try:
            invite = general_channel.create_invite(
                max_age=300,
                max_uses=1,
                unique=True,
            )
        except:
            return msg.edit(
                u':no_entry_sign: Hmmm, something went wrong creating an invite for {}'
                .format(guild.name, ))

        msg.edit(u'Ok, here is a temporary invite for you: {}'.format(
            invite.code, ))

    @Plugin.command('wh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_whitelist(self, event, guild):
        rdb.sadd(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply('Ok, guild %s is now in the whitelist' % guild)

    @Plugin.command('unwh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_unwhitelist(self, event, guild):
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply(
            'Ok, I\'ve made sure guild %s is no longer in the whitelist' %
            guild)

    @Plugin.command('disable', '<plugin:str>', group='plugins', level=-1)
    def plugin_disable(self, event, plugin):
        plugin = self.bot.plugins.get(plugin)
        if not plugin:
            return event.msg.reply(
                'Hmmm, it appears that plugin doesn\'t exist!?')
        self.bot.rmv_plugin(plugin.__class__)
        event.msg.reply('Ok, that plugin has been disabled and unloaded')
예제 #12
0
class CorePlugin(Plugin):
    def load(self, ctx):
        init_db(ENV)

        self.startup = ctx.get('startup', datetime.utcnow())
        self.guilds = ctx.get('guilds', {})
        self.guild_sync = []
        self.guild_sync_debounces = {}

        self.emitter = Emitter()

        super(CorePlugin, self).load(ctx)

        # Overwrite the main bot instances plugin loader so we can magicfy events
        self.bot.add_plugin = self.our_add_plugin

        if ENV != 'prod':
            self.spawn(self.wait_for_plugin_changes)

        self.global_config = None

        with open('config.yaml', 'r') as f:
            self.global_config = safe_load(f)

        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)

    def spawn_wait_for_actions(self, *args, **kwargs):
        self._wait_for_actions_greenlet = self.spawn(self.wait_for_actions)
        self._wait_for_actions_greenlet.link_exception(
            self.spawn_wait_for_actions)

    def our_add_plugin(self, cls, *args, **kwargs):
        if getattr(cls, 'global_plugin', False):
            Bot.add_plugin(self.bot, cls, *args, **kwargs)
            return

        inst = cls(self.bot, None)
        inst.register_trigger('command', 'pre',
                              functools.partial(self.on_pre, inst))
        inst.register_trigger('listener', 'pre',
                              functools.partial(self.on_pre, inst))
        Bot.add_plugin(self.bot, inst, *args, **kwargs)

    def wait_for_plugin_changes(self):
        import gevent_inotifyx as inotify

        fd = inotify.init()
        inotify.add_watch(fd, 'rowboat/plugins/', inotify.IN_MODIFY)
        while True:
            events = inotify.get_events(fd)
            for event in events:
                # Can't reload core.py sadly
                if event.name.startswith('core.py'):
                    continue

                plugin_name = '{}Plugin'.format(
                    event.name.split('.', 1)[0].title())
                plugin = next((v for k, v in self.bot.plugins.items()
                               if k.lower() == plugin_name.lower()), None)
                if plugin:
                    self.log.info('Detected change in %s, reloading...',
                                  plugin_name)
                    try:
                        plugin.reload()
                    except Exception:
                        self.log.exception('Failed to reload: ')

    def wait_for_actions(self):
        ps = rdb.pubsub()
        ps.subscribe('actions')

        for item in ps.listen():
            if item['type'] != 'message':
                continue

            data = json.loads(item['data'])
            if data['type'] == 'GUILD_UPDATE' and data['id'] in self.guilds:
                with self.send_control_message() as embed:
                    embed.title = u'Reloaded config for {}'.format(
                        self.guilds[data['id']].name)

                self.log.info(u'Reloading guild %s',
                              self.guilds[data['id']].name)

                # Refresh config, mostly to validate
                try:
                    config = self.guilds[data['id']].get_config(refresh=True)

                    # Reload the guild entirely
                    self.guilds[data['id']] = Guild.with_id(data['id'])

                    # Update guild access
                    self.update_rowboat_guild_access()

                    # Finally, emit the event
                    self.emitter.emit('GUILD_CONFIG_UPDATE',
                                      self.guilds[data['id']], config)
                except:
                    self.log.exception(u'Failed to reload config for guild %s',
                                       self.guilds[data['id']].name)
                    continue
            elif data['type'] == 'RESTART':
                self.log.info('Restart requested, signaling parent')
                os.kill(os.getppid(), signal.SIGUSR1)
            elif data['type'] == 'GUILD_DELETE':
                name = self.guilds[data['id']].name if self.guilds.has_key(
                    data['id']) else Guild.with_id(data['id']).name
                with self.send_control_message() as embed:
                    embed.color = 0xff6961
                    embed.title = u'Guild Force Deleted {}'.format(name, )

                try:
                    self.log.info(u'Leaving guild %s', name)
                    self.bot.client.api.users_me_guilds_delete(
                        guild=data['id'])
                except:
                    self.log.info(u'Cannot leave guild %s, bot not in guild',
                                  name)
                finally:
                    self.log.info(u'Disabling guild %s', name)
                    Guild.update(enabled=False).where(
                        Guild.guild_id == data['id']).execute()

                    self.log.info(u'Unwhilelisting guild %s', name)
                    rdb.srem(GUILDS_WAITING_SETUP_KEY, str(data['id']))

    def unload(self, ctx):
        ctx['guilds'] = self.guilds
        ctx['startup'] = self.startup
        super(CorePlugin, self).unload(ctx)

    def update_rowboat_guild_access(self):
        # if ROWBOAT_GUILD_ID not in self.state.guilds or ENV != 'prod':
        if ROWBOAT_GUILD_ID not in self.state.guilds or ENV not in ('prod',
                                                                    'docker'):
            return

        rb_guild = self.state.guilds.get(ROWBOAT_GUILD_ID)
        if not rb_guild:
            return

        self.log.info('Updating Jetski guild access')

        guilds = Guild.select(Guild.guild_id, Guild.config).where(
            (Guild.enabled == 1))

        users_who_should_have_access = set()
        for guild in guilds:
            if 'web' not in guild.config:
                continue

            for user_id in guild.config['web'].keys():
                try:
                    users_who_should_have_access.add(int(user_id))
                except:
                    self.log.warning('Guild %s has invalid user ACLs: %s',
                                     guild.guild_id, guild.config['web'])

        # TODO: sharding
        users_who_have_access = {
            i.id
            for i in rb_guild.members.values()
            if ROWBOAT_USER_ROLE_ID in i.roles
        }

        remove_access = set(users_who_have_access) - set(
            users_who_should_have_access)
        add_access = set(users_who_should_have_access) - set(
            users_who_have_access)

        for user_id in remove_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.remove_role(ROWBOAT_USER_ROLE_ID)

        for user_id in add_access:
            member = rb_guild.members.get(user_id)
            if not member:
                continue

            member.add_role(ROWBOAT_USER_ROLE_ID)

    def on_pre(self, plugin, func, event, args, kwargs):
        """
        This function handles dynamically dispatching and modifying events based
        on a specific guilds configuration. It is called before any handler of
        either commands or listeners.
        """
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        if guild_id not in self.guilds:
            if isinstance(event, CommandEvent):
                if event.command.metadata.get('global_', False):
                    return event
            elif hasattr(func, 'subscriptions'):
                if func.subscriptions[0].metadata.get('global_', False):
                    return event

            return

        if hasattr(plugin, 'WHITELIST_FLAG'):
            if not int(
                    plugin.WHITELIST_FLAG) in self.guilds[guild_id].whitelist:
                return

        event.base_config = self.guilds[guild_id].get_config()
        if not event.base_config:
            return

        plugin_name = plugin.name.lower().replace('plugin', '')
        if not getattr(event.base_config.plugins, plugin_name, None):
            return

        self._attach_local_event_data(event, plugin_name, guild_id)

        return event

    def get_config(self, guild_id, *args, **kwargs):
        # Externally Used
        return self.guilds[guild_id].get_config(*args, **kwargs)

    def get_guild(self, guild_id):
        # Externally Used
        return self.guilds[guild_id]

    def _attach_local_event_data(self, event, plugin_name, guild_id):
        if not hasattr(event, 'config'):
            event.config = LocalProxy()

        if not hasattr(event, 'rowboat_guild'):
            event.rowboat_guild = LocalProxy()

        event.config.set(getattr(event.base_config.plugins, plugin_name))
        event.rowboat_guild.set(self.guilds[guild_id])

    @Plugin.schedule(290, init=False)
    def update_guild_bans(self):
        to_update = [
            guild
            for guild in Guild.select().where((
                Guild.last_ban_sync < (datetime.utcnow() - timedelta(days=1)))
                                              | (Guild.last_ban_sync >> None))
            if guild.guild_id in self.client.state.guilds
        ]

        # Update 10 at a time
        for guild in to_update[:10]:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

    @Plugin.listen('GuildUpdate')
    def on_guild_update(self, event):
        self.log.info('Got guild update for guild %s (%s)', event.guild.id,
                      event.guild.channels)

    @Plugin.listen('GuildMembersChunk')
    def on_guild_members_chunk(self, event):
        self.log.info('Got members chunk ({}) for guild {}'.format(
            len(event.members), event.guild_id))
        # for user in event.members:
        #     if user.user.username is UNSET:
        #         # self.log.info('User chunk {} for guild {} has empty values, attempting to patch'.format(user.user.id, event.guild_id))
        #         data = self.bot.client.api.http(Routes.USERS_GET, dict(user=user.user.id))
        #         disco_user = DiscoUser.create(self.bot.client.api.client, data.json())
        #         self.bot.client.state.users[user.id].inplace_update(disco_user)

    @Plugin.listen('GuildBanAdd')
    def on_guild_ban_add(self, event):
        GuildBan.ensure(self.client.state.guilds.get(event.guild_id),
                        event.user)

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        GuildBan.delete().where((GuildBan.user_id == event.user.id)
                                & (GuildBan.guild_id == event.guild_id))

    @contextlib.contextmanager
    def send_control_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Jetski {}'.format('Production' if ENV ==
                                                 'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_CONTROL_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send control message:')
            return

    @contextlib.contextmanager
    def send_spam_control_message(self):
        embed = MessageEmbed()
        embed.set_footer(text='Jetski {}'.format('Production' if ENV ==
                                                 'prod' else 'Testing'))
        embed.timestamp = datetime.utcnow().isoformat()
        embed.color = 0x779ecb
        try:
            yield embed
            self.bot.client.api.channels_messages_create(
                ROWBOAT_SPAM_CONTROL_CHANNEL, embed=embed)
        except:
            self.log.exception('Failed to send spam control message:')
            return

    @Plugin.listen('Resumed')
    def on_resumed(self, event):
        Notification.dispatch(
            Notification.Types.RESUME,
            env=ENV,
        )

        with self.send_control_message() as embed:
            embed.title = 'Resumed'
            embed.color = 0xffb347
            embed.add_field(name='Replayed Events',
                            value=str(self.client.gw.replayed_events))

    @Plugin.listen('Ready', priority=Priority.BEFORE)
    def on_ready(self, event):
        reconnects = self.client.gw.reconnects
        self.log.info('Started session %s', event.session_id)
        Notification.dispatch(
            Notification.Types.CONNECT,
            env=ENV,
        )

        with self.send_control_message() as embed:
            if reconnects:
                embed.title = 'Reconnected'
                embed.color = 0xffb347
            else:
                embed.title = 'Connected'
                embed.color = 0x77dd77

            embed.add_field(name='Gateway Version',
                            value='v{}'.format(event.version),
                            inline=False)
            embed.add_field(name='Session ID',
                            value=event.session_id,
                            inline=False)

    @Plugin.schedule(45, init=False, repeat=False)
    def update_guild_syncs(self):
        if len(self.guild_sync) == 0:
            return

        guilds = [i for i in self.guild_sync]  # hacky deepcopy basically
        for guild_id in guilds:
            if guild_id in self.guild_sync_debounces:
                if self.guild_sync_debounces.get(guild_id) > time.time():
                    self.guild_sync_debounces.pop(guild_id)
                    guilds.remove(guild_id)
            else:
                self.guild_sync_debounces[guild_id] = time.time() + 300
                self.guild_sync.remove(guild_id)

        self.log.info('Requesting Guild Member States for {} guilds'.format(
            len(guilds)))
        self.bot.client.gw.request_guild_members(guild_id_or_ids=guilds)

    @Plugin.listen('GuildCreate',
                   priority=Priority.BEFORE,
                   conditional=lambda e: not e.created)
    def on_guild_create(self, event):
        try:
            guild = Guild.with_id(event.id)
        except Guild.DoesNotExist:
            # If the guild is not awaiting setup, leave it now
            if not rdb.sismember(GUILDS_WAITING_SETUP_KEY, str(
                    event.id)) and event.id != ROWBOAT_GUILD_ID:
                self.log.warning(
                    'Leaving guild %s (%s), not within setup list', event.id,
                    event.name)
                event.guild.leave()
            return

        if not guild.enabled:
            if rdb.sismember(GUILDS_WAITING_SETUP_KEY, str(event.id)):
                guild.enabled = True
                guild.save()
            else:
                return

        config = guild.get_config()
        if not config:
            return

        # Ensure we're updated
        # self.log.info('Syncing guild %s', event.guild.id)
        # guild.sync(event.guild)
        self.log.info('Adding guild {} to sync list'.format(event.id))
        self.guild_sync.append(event.id)

        self.guilds[event.id] = guild

        if config.nickname:

            def set_nickname():
                m = event.members.select_one(id=self.state.me.id)
                if m and m.nick != config.nickname:
                    try:
                        m.set_nickname(config.nickname)
                    except APIException as e:
                        self.log.warning(
                            'Failed to set nickname for guild %s (%s)',
                            event.guild, e.content)

            self.spawn_later(5, set_nickname)

    def get_level(self, guild, user):
        config = (guild.id in self.guilds
                  and self.guilds.get(guild.id).get_config())

        user_level = 0
        if config:
            member = guild.get_member(user)
            if not member:
                return user_level

            for oid in member.roles:
                if oid in config.levels and config.levels[oid] > user_level:
                    user_level = config.levels[oid]

            # User ID overrides should override all others
            if member.id in config.levels:
                user_level = config.levels[member.id]

        return user_level

    @Plugin.listen('MessageCreate')
    def on_message_create(self, event, is_tag=False):
        """
        This monstrosity of a function handles the parsing and dispatching of
        commands.
        """
        # Ignore messages sent by bots
        if event.message.author.bot:
            return

        if rdb.sismember('ignored_channels', event.message.channel_id):
            return

        # If this is message for a guild, grab the guild object
        if hasattr(event, 'guild') and event.guild:
            guild_id = event.guild.id
        elif hasattr(event, 'guild_id') and event.guild_id:
            guild_id = event.guild_id
        else:
            guild_id = None

        guild = self.guilds.get(event.guild.id) if guild_id else None
        config = guild and guild.get_config()
        cc = config.commands if config else None

        # If the guild has configuration, use that (otherwise use defaults)
        if config and config.commands:
            commands = list(
                self.bot.get_commands_for_message(config.commands.mention, {},
                                                  config.commands.prefix,
                                                  event.message))
        elif guild_id:
            # Otherwise, default to requiring mentions
            commands = list(
                self.bot.get_commands_for_message(True, {}, '', event.message))
        else:
            # if ENV != 'prod':
            if ENV not in ('prod', 'docker'):
                if not event.message.content.startswith(ENV + '!'):
                    return
                event.message.content = event.message.content[len(ENV) + 1:]

            # DM's just use the commands (no prefix/mention)
            commands = list(
                self.bot.get_commands_for_message(False, {}, '',
                                                  event.message))

        # if no command, attempt to run as a tag
        if not commands:
            if is_tag:
                return

            if not event.guild or not self.bot.plugins.get('TagsPlugin'):
                return

            if not config or not config.plugins or not config.plugins.tags:
                return

            prefixes = []
            if cc.prefix:
                prefixes.append(cc.prefix)
            if cc.mention:
                prefixes.append('{} '.format(self.state.me.mention))
            if not prefixes:
                return

            tag_re = re.compile('^({})(.+)'.format(
                re.escape('|'.join(prefixes))))
            m = tag_re.match(event.message.content)
            if not m:
                return

            sqlplugin = self.bot.plugins.get('SQLPlugin')
            if sqlplugin:
                sqlplugin.tag_messages.append(event.message.id)
            event.message.content = u'{}tags show {}'.format(
                m.group(1), m.group(2))
            return self.on_message_create(event, True)

        event.user_level = self.get_level(event.guild,
                                          event.author) if event.guild else 0

        # Grab whether this user is a global admin
        # TODO: cache this
        global_admin = rdb.sismember('global_admins', event.author.id)

        # Iterate over commands and find a match
        for command, match in commands:
            if command.level == -1 and not global_admin:
                continue

            level = command.level

            if guild and not config and command.triggers[0] != 'setup':
                continue
            elif config and config.commands and command.plugin != self:
                overrides = {}
                for obj in config.commands.get_command_override(command):
                    overrides.update(obj)

                if overrides.get('disabled'):
                    continue

                level = overrides.get('level', level)

            if not global_admin and event.user_level < level:
                continue

            with timed('rowboat.command.duration',
                       tags={
                           'plugin': command.plugin.name,
                           'command': command.name
                       }):
                try:
                    command_event = CommandEvent(command, event.message, match)
                    command_event.user_level = event.user_level
                    command.plugin.execute(command_event)
                except CommandResponse as e:
                    if is_tag:
                        return
                    event.reply(e.response)
                except:
                    tracked = Command.track(event, command, exception=True)
                    self.log.exception('Command error:')

                    with self.send_control_message() as embed:
                        embed.title = u'Command Error: {}'.format(command.name)
                        embed.color = 0xff6961
                        embed.add_field(name='Author',
                                        value='({}) `{}`'.format(
                                            event.author, event.author.id),
                                        inline=True)
                        embed.add_field(name='Channel',
                                        value='({}) `{}`'.format(
                                            event.channel.name,
                                            event.channel.id),
                                        inline=True)
                        embed.description = '```{}```'.format(u'\n'.join(
                            tracked.traceback.split('\n')[-8:]))

                    return event.reply(
                        '<:{}> something went wrong, perhaps try again later'.
                        format(RED_TICK_EMOJI))

            Command.track(event, command)

            # Dispatch the command used modlog event
            if config:
                modlog_config = getattr(config.plugins, 'modlog', None)
                if not modlog_config:
                    return

                if is_tag:  # Yes, I know, this is ugly but I don't have better
                    event.content = event.content.replace('tags show ', '', 1)

                self._attach_local_event_data(event, 'modlog', event.guild.id)

                plugin = self.bot.plugins.get('ModLogPlugin')
                if plugin:
                    plugin.log_action(Actions.COMMAND_USED, event)

            return

    @Plugin.command('setup')
    def command_setup(self, event):
        if not event.guild:
            return event.msg.reply(
                ':warning: this command can only be used in servers')

        # Make sure we're not already setup
        if event.guild.id in self.guilds:
            return event.msg.reply(':warning: this server is already setup')

        global_admin = rdb.sismember('global_admins', event.author.id)

        # Make sure this is the owner of the server
        if not global_admin:
            if not event.guild.owner_id == event.author.id:
                return event.msg.reply(
                    ':warning: only the server owner can setup Jetski')

        # Make sure we have admin perms
        m = event.guild.members.select_one(id=self.state.me.id)
        if not m.permissions.administrator and not global_admin:
            return event.msg.reply(
                ':warning: bot must have the Administrator permission')

        guild = Guild.setup(event.guild)
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(event.guild.id))
        self.guilds[event.guild.id] = guild
        event.msg.reply(':ok_hand: successfully loaded configuration')

    @Plugin.command('nuke', '<user:snowflake> <reason:str...>', level=-1)
    def nuke(self, event, user, reason):
        contents = []

        for gid, guild in self.guilds.items():
            guild = self.state.guilds[gid]
            perms = guild.get_permissions(self.state.me)

            if not perms.ban_members and not perms.administrator:
                contents.append(u':x: {} - No Permissions'.format(guild.name))
                continue

            try:
                Infraction.ban(self.bot.plugins.get('AdminPlugin'),
                               event,
                               user,
                               reason,
                               guild=guild)
            except:
                contents.append(u':x: {} - Unknown Error'.format(guild.name))
                self.log.exception('Failed to force ban %s in %s', user, gid)

            contents.append(
                u':white_check_mark: {} - :regional_indicator_f:'.format(
                    guild.name))

        event.msg.reply('Results:\n' + '\n'.join(contents))

    @Plugin.command('about')
    def command_about(self, event):
        embed = MessageEmbed()
        embed.color = 0x7289da
        embed.set_author(name='Jetski',
                         icon_url=self.client.state.me.avatar_url,
                         url='https://jetski.ga/')
        embed.description = BOT_INFO
        embed.add_field(name='Servers',
                        value=str(Guild.select().count()),
                        inline=True)
        embed.add_field(name='Uptime',
                        value=humanize_duration(datetime.utcnow() -
                                                self.startup),
                        inline=True)
        event.msg.reply(embed=embed)

    @Plugin.command('uptime', level=-1)
    def command_uptime(self, event):
        event.msg.reply('Jetski was started {} ago.'.format(
            humanize_duration(datetime.utcnow() - self.startup)))

    @Plugin.command('source', '<command>', level=-1)
    def command_source(self, event, command=None):
        for cmd in self.bot.commands:
            if command.lower() in cmd.triggers:
                break
        else:
            event.msg.reply(u"Couldn't find command for `{}`".format(
                S(command, escape_codeblocks=True)))
            return

        code = cmd.func.__code__
        lines, firstlineno = inspect.getsourcelines(code)

        event.msg.reply(
            '<https://github.com/ThaTiemsz/jetski/blob/master/{}#L{}-{}>'.
            format(code.co_filename, firstlineno, firstlineno + len(lines)))

    @Plugin.command('eval', level=-1)
    def command_eval(self, event):
        ctx = {
            'bot': self.bot,
            'client': self.bot.client,
            'state': self.bot.client.state,
            'event': event,
            'msg': event.msg,
            'guild': event.msg.guild,
            'channel': event.msg.channel,
            'author': event.msg.author
        }

        # Mulitline eval
        src = event.codeblock
        if src.count('\n'):
            lines = filter(bool, src.split('\n'))
            if lines[-1] and 'return' not in lines[-1]:
                lines[-1] = 'return ' + lines[-1]
            lines = '\n'.join('    ' + i for i in lines)
            code = 'def f():\n{}\nx = f()'.format(lines)
            local = {}

            try:
                exec compile(code, '<eval>', 'exec') in ctx, local
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

            result = pprint.pformat(local['x'])
        else:
            try:
                result = str(eval(src, ctx))
            except Exception as e:
                event.msg.reply(
                    PY_CODE_BLOCK.format(type(e).__name__ + ': ' + str(e)))
                return

        if len(result) > 1990:
            event.msg.reply('', attachments=[('result.txt', result)])
        else:
            event.msg.reply(PY_CODE_BLOCK.format(result))

    @Plugin.command('sync-bans', group='control', level=-1)
    def control_sync_bans(self, event):
        guilds = list(Guild.select().where(Guild.enabled == 1))

        msg = event.msg.reply(':timer: pls wait while I sync...')

        for guild in guilds:
            guild.sync_bans(self.client.state.guilds.get(guild.guild_id))

        msg.edit('<:{}> synced {} guilds'.format(GREEN_TICK_EMOJI,
                                                 len(guilds)))

    @Plugin.command('reconnect', group='control', level=-1)
    def control_reconnect(self, event):
        event.msg.reply('Ok, closing connection')
        self.client.gw.ws.close()

    @Plugin.command('invite', '<guild:snowflake>', group='guilds', level=-1)
    def guild_join(self, event, guild):
        guild = self.state.guilds.get(guild)
        if not guild:
            return event.msg.reply(
                ':no_entry_sign: invalid or unknown guild ID')

        msg = event.msg.reply(
            u'Ok, hold on while I get you setup with an invite link to {}'.
            format(guild.name, ))

        general_channel = guild.channels[guild.id]

        try:
            invite = general_channel.create_invite(
                max_age=300,
                max_uses=1,
                unique=True,
            )
        except:
            return msg.edit(
                u':no_entry_sign: Hmmm, something went wrong creating an invite for {}'
                .format(guild.name, ))

        msg.edit(u'Ok, here is a temporary invite for you: {}'.format(
            invite.code, ))

    @Plugin.command('wh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_whitelist(self, event, guild):
        rdb.sadd(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply('Ok, guild %s is now in the whitelist' % guild)

    @Plugin.command('unwh', '<guild:snowflake>', group='guilds', level=-1)
    def guild_unwhitelist(self, event, guild):
        rdb.srem(GUILDS_WAITING_SETUP_KEY, str(guild))
        event.msg.reply(
            'Ok, I\'ve made sure guild %s is no longer in the whitelist' %
            guild)

    @Plugin.command('wh-add',
                    '<guild:snowflake> <flag:str>',
                    group='guilds',
                    level=-1)
    def add_whitelist(self, event, guild, flag):
        flag = Guild.WhitelistFlags.get(flag)
        if not flag:
            raise CommandFail('invalid flag')

        try:
            guild = Guild.get(guild_id=guild)
        except Guild.DoesNotExist:
            raise CommandFail('no guild exists with that id')

        if guild.is_whitelisted(flag):
            raise CommandFail('this guild already has this flag')

        guild.whitelist.append(int(flag))
        guild.save()
        guild.emit('GUILD_UPDATE')

        event.msg.reply('Ok, added flag `{}` to guild {}'.format(
            str(flag), guild.guild_id))

    @Plugin.command('wh-rmv',
                    '<guild:snowflake> <flag:str>',
                    group='guilds',
                    level=-1)
    def rmv_whitelist(self, event, guild, flag):
        flag = Guild.WhitelistFlags.get(flag)
        if not flag:
            raise CommandFail('invalid flag')

        try:
            guild = Guild.get(guild_id=guild)
        except Guild.DoesNotExist:
            raise CommandFail('no guild exists with that id')

        if not guild.is_whitelisted(flag):
            raise CommandFail('this guild doesn\'t have this flag')

        guild.whitelist.remove(int(flag))
        guild.save()
        guild.emit('GUILD_UPDATE')

        event.msg.reply('Ok, removed flag `{}` from guild {}'.format(
            str(flag), guild.guild_id))

    @Plugin.command('plugins',
                    aliases=['pl', 'plugin'],
                    context={'mode': 'list'},
                    level=-1)
    @Plugin.command('list',
                    group='plugins',
                    aliases=['ls'],
                    context={'mode': 'list'},
                    level=-1)
    @Plugin.command('load',
                    '<plugin:str>',
                    group='plugins',
                    aliases=['enable'],
                    context={'mode': 'load'},
                    level=-1)
    @Plugin.command('unload',
                    '<plugin:str>',
                    group='plugins',
                    aliases=['disable'],
                    context={'mode': 'unload'},
                    level=-1)
    @Plugin.command('reload',
                    '<plugin:str>',
                    group='plugins',
                    aliases=['restart'],
                    context={'mode': 'reload'},
                    level=-1)
    def plugin_manager(self, event, plugin=None, mode='list'):
        if mode == 'list':
            embed = MessageEmbed()
            embed.color = 0x7289da
            embed.set_author(name='Loaded Plugins ({})'.format(
                len(self.bot.plugins)),
                             icon_url=self.state.me.avatar_url)
            embed.description = '```md\n{}```'.format('\n'.join(
                '- {}'.format(key) for key in self.bot.plugins))
            embed.set_footer(
                text=
                'Use file name for load, registered name for unload/reload.')
            return event.msg.reply('', embed=embed)

        pl = self.bot.plugins.get(plugin)

        if mode == 'load':
            if pl:
                return event.msg.reply(
                    '<:{}> {} plugin is already loaded.'.format(
                        RED_TICK_EMOJI, pl.name))
            try:
                self.bot.add_plugin_module('rowboat.plugins.{}'.format(plugin))
            except:
                return event.msg.reply(
                    '<:{}> Failed to load {} plugin.'.format(
                        RED_TICK_EMOJI, plugin))
            return event.msg.reply(
                ':ok_hand: Loaded {} plugin.'.format(plugin))

        if not pl:
            return event.msg.reply(
                '<:{}> Could not find this plugin.'.format(plugin))

        if mode == 'unload':
            self.bot.rmv_plugin(pl.__class__)
            self.log.info('Unloaded {} plugin'.format(pl.name))
            return event.msg.reply(':ok_hand: Unloaded {} plugin.'.format(
                pl.name))

        if mode == 'reload':
            self.bot.reload_plugin(pl.__class__)
            self.log.info('Reloaded {} plugin'.format(pl.name))
            return event.msg.reply(':ok_hand: Reloaded {} plugin.'.format(
                pl.name))
예제 #13
0
class VoiceClient(LoggingClass):
    VOICE_GATEWAY_VERSION = 4

    SUPPORTED_MODES = {
        'xsalsa20_poly1305_lite',
        'xsalsa20_poly1305_suffix',
        'xsalsa20_poly1305',
    }

    def __init__(self,
                 client,
                 server_id,
                 is_dm=False,
                 encoder=None,
                 max_reconnects=5):
        super(VoiceClient, self).__init__()

        self.client = client
        self.server_id = server_id
        self.channel_id = None
        self.is_dm = is_dm
        self.encoder = encoder or JSONEncoder
        self.max_reconnects = max_reconnects
        self.video_enabled = False

        # Set the VoiceClient in the state's voice clients
        self.client.state.voice_clients[self.server_id] = self

        # Bind to some WS packets
        self.packets = Emitter()
        self.packets.on(VoiceOPCode.HELLO, self.on_voice_hello)
        self.packets.on(VoiceOPCode.READY, self.on_voice_ready)
        self.packets.on(VoiceOPCode.RESUMED, self.on_voice_resumed)
        self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp)
        self.packets.on(VoiceOPCode.SPEAKING, self.on_voice_speaking)
        self.packets.on(VoiceOPCode.CLIENT_CONNECT,
                        self.on_voice_client_connect)
        self.packets.on(VoiceOPCode.CLIENT_DISCONNECT,
                        self.on_voice_client_disconnect)
        self.packets.on(VoiceOPCode.CODECS, self.on_voice_codecs)

        # State + state change emitter
        self.state = VoiceState.DISCONNECTED
        self.state_emitter = Emitter()

        # Connection metadata
        self.token = None
        self.endpoint = None
        self.ssrc = None
        self.ip = None
        self.port = None
        self.mode = None
        self.udp = None
        self.audio_codec = None
        self.video_codec = None
        self.transport_id = None

        # Websocket connection
        self.ws = None

        self._session_id = self.client.gw.session_id
        self._reconnects = 0
        self._heartbeat_task = None
        self._identified = False

        # SSRCs
        self.audio_ssrcs = {}

    def __repr__(self):
        return u'<VoiceClient {}>'.format(self.server_id)

    @cached_property
    def guild(self):
        return self.client.state.guilds.get(
            self.server_id) if not self.is_dm else None

    @cached_property
    def channel(self):
        return self.client.state.channels.get(self.channel_id)

    @property
    def user_id(self):
        return self.client.state.me.id

    @property
    def ssrc_audio(self):
        return self.ssrc

    @property
    def ssrc_video(self):
        return self.ssrc + 1

    @property
    def ssrc_rtx(self):
        return self.ssrc + 2

    @property
    def ssrc_rtcp(self):
        return self.ssrc + 3

    def set_state(self, state):
        self.log.debug('[%s] state %s -> %s', self, self.state, state)
        prev_state = self.state
        self.state = state
        self.state_emitter.emit(state, prev_state)

    def set_endpoint(self, endpoint):
        endpoint = endpoint.split(':', 1)[0]
        if self.endpoint == endpoint:
            return

        self.log.info(
            '[%s] Set endpoint from VOICE_SERVER_UPDATE (state = %s / endpoint = %s)',
            self, self.state, endpoint)

        self.endpoint = endpoint

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()
            self.ws = None

        self._identified = False

    def set_token(self, token):
        if self.token == token:
            return
        self.token = token
        if not self._identified:
            self._connect_and_run()

    def _connect_and_run(self):
        self.ws = Websocket('wss://' + self.endpoint +
                            '/?v={}'.format(self.VOICE_GATEWAY_VERSION))
        self.ws.emitter.on('on_open', self.on_open)
        self.ws.emitter.on('on_error', self.on_error)
        self.ws.emitter.on('on_close', self.on_close)
        self.ws.emitter.on('on_message', self.on_message)
        self.ws.run_forever()

    def _heartbeat(self, interval):
        while True:
            self.send(VoiceOPCode.HEARTBEAT, time.time())
            gevent.sleep(interval / 1000)

    def set_speaking(self,
                     voice=False,
                     soundshare=False,
                     priority=False,
                     delay=0):
        value = SpeakingFlags.NONE.value
        if voice:
            value |= SpeakingFlags.VOICE.value
        if soundshare:
            value |= SpeakingFlags.SOUNDSHARE.value
        if priority:
            value |= SpeakingFlags.PRIORITY.value

        self.send(VoiceOPCode.SPEAKING, {
            'speaking': value,
            'delay': delay,
            'ssrc': self.ssrc,
        })

    def set_voice_state(self, channel_id, mute=False, deaf=False, video=False):
        self.client.gw.send(
            OPCode.VOICE_STATE_UPDATE, {
                'self_mute': bool(mute),
                'self_deaf': bool(deaf),
                'self_video': bool(video),
                'guild_id': None if self.is_dm else self.server_id,
                'channel_id': channel_id,
            })

    def send(self, op, data):
        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.log.debug('[%s] sending OP %s (data = %s)', self, op, data)
            self.ws.send(self.encoder.encode({
                'op': op.value,
                'd': data,
            }), self.encoder.OPCODE)
        else:
            self.log.debug(
                '[%s] dropping because ws is closed OP %s (data = %s)', self,
                op, data)

    def on_voice_client_connect(self, data):
        user_id = int(data['user_id'])

        self.audio_ssrcs[data['audio_ssrc']] = user_id
        # ignore data['voice_ssrc'] for now

    def on_voice_client_disconnect(self, data):
        user_id = int(data['user_id'])

        for ssrc in self.audio_ssrcs.keys():
            if self.audio_ssrcs[ssrc] == user_id:
                del self.audio_ssrcs[ssrc]
                break

    def on_voice_codecs(self, data):
        self.audio_codec = data['audio_codec']
        self.video_codec = data['video_codec']
        self.transport_id = data['media_session_id']

        # Set the UDP's RTP Audio Header's Payload Type
        self.udp.set_audio_codec(data['audio_codec'])

    def on_voice_hello(self, data):
        self.log.info(
            '[%s] Received Voice HELLO payload, starting heartbeater', self)
        self._heartbeat_task = gevent.spawn(self._heartbeat,
                                            data['heartbeat_interval'])
        self.set_state(VoiceState.AUTHENTICATED)

    def on_voice_ready(self, data):
        self.log.info(
            '[%s] Received Voice READY payload, attempting to negotiate voice connection w/ remote',
            self)
        self.set_state(VoiceState.CONNECTING)
        self.ssrc = data['ssrc']
        self.ip = data['ip']
        self.port = data['port']
        self._identified = True

        for mode in self.SUPPORTED_MODES:
            if mode in data['modes']:
                self.mode = mode
                self.log.debug('[%s] Selected mode %s', self, mode)
                break
        else:
            raise Exception('Failed to find a supported voice mode')

        self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self,
                       self.ip, self.port)
        self.udp = UDPVoiceClient(self)
        ip, port = self.udp.connect(self.ip, self.port)

        if not ip:
            self.log.error(
                'Failed to discover our IP, perhaps a NAT or firewall is f*****g us'
            )
            self.disconnect()
            return

        codecs = []

        # Sending discord our available codecs and rtp payload type for it
        for idx, codec in enumerate(AudioCodecs):
            codecs.append({
                'name': codec,
                'type': 'audio',
                'priority': (idx + 1) * 1000,
                'payload_type': RTPPayloadTypes.get(codec).value,
            })

        self.log.debug(
            '[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL',
            self, ip, port)
        self.send(
            VoiceOPCode.SELECT_PROTOCOL, {
                'protocol': 'udp',
                'data': {
                    'port': port,
                    'address': ip,
                    'mode': self.mode,
                },
                'codecs': codecs,
            })
        self.send(VoiceOPCode.CLIENT_CONNECT, {
            'audio_ssrc': self.ssrc,
            'video_ssrc': 0,
            'rtx_ssrc': 0,
        })

    def on_voice_resumed(self, data):
        self.log.info('[%s] Received resumed', self)
        self.set_state(VoiceState.CONNECTED)

    def on_voice_sdp(self, sdp):
        self.log.info(
            '[%s] Received session description, connection completed', self)

        self.mode = sdp['mode']
        self.audio_codec = sdp['audio_codec']
        self.video_codec = sdp['video_codec']
        self.transport_id = sdp['media_session_id']

        # Set the UDP's RTP Audio Header's Payload Type
        self.udp.set_audio_codec(sdp['audio_codec'])

        # Create a secret box for encryption/decryption
        self.udp.setup_encryption(bytes(bytearray(sdp['secret_key'])))

        self.set_state(VoiceState.CONNECTED)

    def on_voice_speaking(self, data):
        user_id = int(data['user_id'])

        self.audio_ssrcs[data['ssrc']] = user_id

        # Maybe rename speaking to voice in future
        payload = VoiceSpeaking(
            client=self,
            user_id=user_id,
            speaking=bool(data['speaking'] & SpeakingFlags.VOICE.value),
            soundshare=bool(data['speaking'] & SpeakingFlags.SOUNDSHARE.value),
            priority=bool(data['speaking'] & SpeakingFlags.PRIORITY.value),
        )

        self.client.gw.events.emit('VoiceSpeaking', payload)

    def on_message(self, msg):
        try:
            data = self.encoder.decode(msg)
            self.packets.emit(VoiceOPCode[data['op']], data['d'])
        except Exception:
            self.log.exception('Failed to parse voice gateway message: ')

    def on_error(self, err):
        self.log.error('[%s] Voice websocket error: %s', self, err)

    def on_open(self):
        if self._identified:
            self.send(
                VoiceOPCode.RESUME, {
                    'server_id': self.server_id,
                    'session_id': self._session_id,
                    'token': self.token,
                })
        else:
            self.send(
                VoiceOPCode.IDENTIFY, {
                    'server_id': self.server_id,
                    'user_id': self.user_id,
                    'session_id': self._session_id,
                    'token': self.token,
                    'video': self.video_enabled,
                })

    def on_close(self, code, reason):
        self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self,
                         code, reason, self._reconnects)

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        self.ws = None

        # If we killed the connection, don't try resuming
        if self.state == VoiceState.DISCONNECTED:
            return

        self.log.info('[%s] Attempting Websocket Resumption', self)

        self.set_state(VoiceState.RECONNECTING)

        # Check if code is not None, was not from us
        if code is not None:
            self._reconnects += 1

            if self.max_reconnects and self._reconnects > self.max_reconnects:
                raise VoiceException(
                    'Failed to reconnect after {} attempts, giving up'.format(
                        self.max_reconnects), self)

            # Don't resume for these error codes:
            if 4000 <= code <= 4016:
                self._identified = False

                if self.udp and self.udp.connected:
                    self.udp.disconnect()

            wait_time = 5
        else:
            wait_time = 1

        self.log.info('[%s] Will attempt to %s after %s seconds', self,
                      'resume' if self._identified else 'reconnect', wait_time)
        gevent.sleep(wait_time)

        self._connect_and_run()

    def connect(self, channel_id, timeout=10, **kwargs):
        if self.is_dm:
            channel_id = self.server_id

        if not channel_id:
            raise VoiceException(
                '[{}] cannot connect to an empty channel id'.format(self))

        if self.channel_id == channel_id:
            if self.state == VoiceState.CONNECTED:
                self.log.debug('[%s] Already connected to %s, returning', self,
                               self.channel)
                return self
        else:
            if self.state == VoiceState.CONNECTED:
                self.log.debug('[%s] Moving to channel %s', self, channel_id)
            else:
                self.log.debug('[%s] Attempting connection to channel id %s',
                               self, channel_id)
                self.set_state(VoiceState.AWAITING_ENDPOINT)

        self.set_voice_state(channel_id, **kwargs)

        if not self.state_emitter.once(VoiceState.CONNECTED, timeout=timeout):
            self.disconnect()
            raise VoiceException('Failed to connect to voice', self)
        else:
            return self

    def disconnect(self):
        if self.state == VoiceState.DISCONNECTED:
            return

        self.log.debug('[%s] disconnect called', self)
        self.set_state(VoiceState.DISCONNECTED)

        del self.client.state.voice_clients[self.server_id]

        if self._heartbeat_task:
            self._heartbeat_task.kill()
            self._heartbeat_task = None

        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.close()
            self.ws = None

        if self.udp and self.udp.connected:
            self.udp.disconnect()

        if self.channel_id:
            self.set_voice_state(None)

        self.client.gw.events.emit('VoiceDisconnect', self)

    def send_frame(self, *args, **kwargs):
        self.udp.send_frame(*args, **kwargs)

    def increment_timestamp(self, *args, **kwargs):
        self.udp.increment_timestamp(*args, **kwargs)