Exemple #1
0
    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)
Exemple #2
0
def run_bot():
    from gevent import monkey
    monkey.patch_all()

    #patch_MessageTable()

    parser = argparse.ArgumentParser()
    parser.add_argument('--token', help="discord api auth token", default=None)
    parser.add_argument('--log-level', help='log level', default="INFO")

    if not os.path.exists(CONFIG_FILE):
        print("%s missing, pls fix" % CONFIG_FILE)
        exit()

    config = ClientConfig.from_file(CONFIG_FILE)

    args = parser.parse_args()

    if args.log_level:
        setup_logging(level=getattr(logging, args.log_level.upper()))

    if args.token is not None:
        config.token = args.token

    if not is_valid_token(config.token):
        print("the token '%s' isn't valid" % config.token)
        exit()

    client = Client(config)

    bot_config = BotConfig(config.bot)

    # get plugin files
    # todo: support multilevel nested plugins
    _, _, filenames = next(os.walk("plugins"), (None, None, []))
    filenames.remove("__init__.py")

    # convert plugins from ayylmao.py to plugins.ayylmao
    filenames = ["plugins.%s" % os.path.splitext(p)[0] for p in filenames]

    # remove disabled plugins from plugin array
    filenames = [p for p in filenames if p not in config.disabled_plugins]

    bot_config.plugins = filenames

    if len(bot_config.plugins) > 0:
        print("discovered plugins:")

        for p in bot_config.plugins:
            print(" - %s" % p)
    else:
        print("no plugins found")

    bot = Bot(client, bot_config)

    bot.run_forever()
Exemple #3
0
    def __init__(self):
        setup_logging(level=logging.WARNING)

        self.client_config = ClientConfig()
        self.client_config.token = app.config['DISCORD_BOT_TOKEN']

        self.client = Client(self.client_config)

        self.bot = Bot(self.client)
        self.bot.add_plugin(RwrsBotDiscoPlugin)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if 'token' not in self.serverdata:
            raise ProtocolError("No API token defined under server settings")
        self.client_config = ClientConfig({'token': self.serverdata['token']})
        self.client = Client(self.client_config)
        self.bot_config = BotConfig()
        self.bot = Bot(self.client, self.bot_config)
        self.bot_plugin = DiscordBotPlugin(self, self.bot, self.bot_config)
        self.bot.add_plugin(self.bot_plugin)
        #setup_logging(level='DEBUG')
        self._children = {}
        self.message_queue = queue.Queue()
Exemple #5
0
def disco_main(run=False):
    """
    Creates an argument parser and parses a standard set of command line arguments,
    creating a new :class:`Client`.

    Returns
    -------
    :class:`Client`
        A new Client from the provided command line arguments
    """
    from disco.client import Client, ClientConfig
    from disco.bot import Bot, BotConfig
    from disco.util.logging import setup_logging

    # Parse out all our command line arguments
    args = parser.parse_args()

    # Create the base configuration object
    if args.config:
        config = ClientConfig.from_file(args.config)
    else:
        if os.path.exists('config.json'):
            config = ClientConfig.from_file('config.json')
        elif os.path.exists('config.yaml'):
            config = ClientConfig.from_file('config.yaml')
        else:
            config = ClientConfig()

    for arg_key, config_key in six.iteritems(CONFIG_OVERRIDE_MAPPING):
        if getattr(args, arg_key) is not None:
            setattr(config, config_key, getattr(args, arg_key))

    # Setup the auto-sharder
    if args.shard_auto:
        from disco.gateway.sharder import AutoSharder
        AutoSharder(config).run()
        return

    # Setup logging based on the configured level
    setup_logging(level=getattr(logging, config.log_level.upper()))

    # Build out client object
    client = Client(config)

    # If applicable, build the bot and load plugins
    bot = None
    if args.run_bot or hasattr(config, 'bot'):
        bot_config = BotConfig(config.bot) if hasattr(config,
                                                      'bot') else BotConfig()
        if not hasattr(bot_config, 'plugins'):
            bot_config.plugins = args.plugin
        else:
            bot_config.plugins += args.plugin

        bot = Bot(client, bot_config)

    if run:
        (bot or client).run_forever()

    return bot or client
Exemple #6
0
class RwrsBot:
    """The high-level class that wraps the RWRS Discord bot logic."""
    def __init__(self):
        setup_logging(level=logging.WARNING)

        self.client_config = ClientConfig()
        self.client_config.token = app.config['DISCORD_BOT_TOKEN']

        self.client = Client(self.client_config)

        self.bot = Bot(self.client)
        self.bot.add_plugin(RwrsBotDiscoPlugin)

    def run(self):
        """Actually run the RWRS Discord bot."""
        self.bot.run_forever()
Exemple #7
0
 def start_disco(self):
     setup_logging(level=logging.DEBUG)
     self.config = ClientConfig.from_file("./secrets/discord.json")
     self.client = Client(self.config)
     self.bot_config = BotConfig(self.config.bot)
     self.bot = Bot(self.client, self.bot_config)
     self.bot.main_process_recv = self.main_process_recv
     return self.bot.client.run()
Exemple #8
0
def disco_main(run=False):
    """
    Creates an argument parser and parses a standard set of command line arguments,
    creating a new :class:`Client`.

    Returns
    -------
    :class:`Client`
        A new Client from the provided command line arguments
    """
    args = parser.parse_args()

    from disco.client import Client, ClientConfig
    from disco.bot import Bot, BotConfig
    from disco.util.token import is_valid_token
    from disco.util.logging import setup_logging

    if os.path.exists(args.config):
        config = ClientConfig.from_file(args.config)
    else:
        config = ClientConfig()

    for k, v in six.iteritems(vars(args)):
        if hasattr(config, k) and v is not None:
            setattr(config, k, v)

    if not is_valid_token(config.token):
        print('Invalid token passed')
        return

    if args.shard_auto:
        from disco.gateway.sharder import AutoSharder
        AutoSharder(config).run()
        return

    if hasattr(config, 'logging_format'):
        setup_logging(level=logging.INFO, format=config.logging_format)
    else:
        setup_logging(level=logging.INFO)

    client = Client(config)

    bot = None
    if args.run_bot or hasattr(config, 'bot'):
        bot_config = BotConfig(config.bot) if hasattr(config, 'bot') else BotConfig()
        if not hasattr(bot_config, 'plugins'):
            bot_config.plugins = args.plugin
        else:
            bot_config.plugins += args.plugin
        bot = Bot(client, bot_config)

    if run:
        (bot or client).run_forever()

    return (bot or client)
Exemple #9
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if 'token' not in self.serverdata:
            raise ProtocolError("No API token defined under server settings")

        client_config = ClientConfig({'token': self.serverdata['token'],
                                      'max_reconnects': 0})
        self.client = Client(client_config)

        bot_config = BotConfig()
        self.bot = Bot(self.client, bot_config)

        self.bot_plugin = DiscordBotPlugin(self, self.bot, bot_config)
        self.bot.add_plugin(self.bot_plugin)

        self._children = {}
        self.message_queue = queue.Queue()
        self.webhooks = {}
        self._message_thread = None
def setup_bot(args, client, config):
    bot_config = BotConfig(config.bot) if hasattr(config,
                                                  'bot') else BotConfig()
    if not hasattr(bot_config, 'plugins'):
        bot_config.plugins = args.plugin
    else:
        bot_config.plugins += args.plugin

    bot_config.commands_require_mention = False

    bot = Bot(client, bot_config)
    return bot, bot_config
Exemple #11
0
def disco_main(run=False):
    """
    Creates an argument parser and parses a standard set of command line arguments,
    creating a new :class:`Client`.

    Returns
    -------
    :class:`Client`
        A new Client from the provided command line arguments
    """

    from gevent import monkey

    monkey.patch_all()

    from disco.client import Client, ClientConfig
    from disco.bot import Bot, BotConfig
    from disco.util.token import is_valid_token
    from disco.util.logging import setup_logging

    config = ClientConfig.from_file('config.yaml')

    if not is_valid_token(config.token):
        print('Invalid token passed')
        return

    setup_logging(level='WARN')

    client = Client(config)

    bot_config = BotConfig(config.bot)

    bot = Bot(client, bot_config)
    from db import ChartingDao
    bot.db = ChartingDao()

    bot.run_forever()
    return bot
Exemple #12
0
    def load(self, config=None, state=None):
        super().load(config=config, state=state)

        conf = ClientConfig()
        conf.token = self.config.get('token')
        conf.max_reconnects = 0
        self.client = Client(conf)
        self.client.parent = self
        self.client.agent = self.agent

        self.bot = None
        bot_config = BotConfig()
        bot_config.commands_require_mention = False
        bot_config.commands_prefix = self.config.get('command_prefix', '.')

        bot_config.levels = self.config.get('access', {})

        bot_config.commands_level_getter = self.level_getter
        self.bot = Bot(self.client, bot_config)

        for unique_id, pconf in self.config.get('plugins').items():
            if pconf is None:
                pconf = {}

            pconf.setdefault("_autoload", True)
            pconf.setdefault("_module", unique_id)
            if not pconf["_autoload"]:
                continue

            self.bot.add_plugin_module(pconf["_module"], pconf)  #TODO: replace

        for _, plugin in self.bot.plugins.items():
            plugin.parent = self

        self.bot.agent = self.agent
        self.bot.parent = self

        self.me = self.client.api.users_me_get()
Exemple #13
0
def run_shard(config, shard_id, pipe):
    setup_logging(
        level=logging.INFO,
        format='{} [%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s'.format(shard_id)
    )

    config.shard_id = shard_id
    client = Client(config)
    bot = Bot(client, BotConfig(config.bot))
    bot.sharder = GIPCProxy(bot, pipe)
    bot.shards = ShardHelper(config.shard_count, bot)
    bot.run_forever()
Exemple #14
0
    def createBot(self):
        # Create the base configuration object
        config = ClientConfig.from_file(
            '/home/nathan/PYTHON/WORKING-USE-THIS!!!/src-3.0/config/discordconfig.json'
        )

        # Setup logging based on the configured level
        setup_logging(level=getattr(logging, config.log_level.upper()))

        # Build our client object
        self.client = Client(config)

        # If applicable, build the bot and load plugins
        bot_config = BotConfig(config.bot)
        self.bot = Bot(self.client, bot_config)
        #self.bot.add_plugin(TutPlugin, config) # This is how we would add plugins, TugPlugin would be a class

        self.apiClient = self.bot.client.api
        self.startMainBotLoop()

        (self.bot or self.client).run_forever()
Exemple #15
0
class PyLinkDiscordProtocol(PyLinkNetworkCoreWithUtils):
    S2S_BUFSIZE = 0

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if 'token' not in self.serverdata:
            raise ProtocolError("No API token defined under server settings")

        client_config = ClientConfig({'token': self.serverdata['token'],
                                      'max_reconnects': 0})
        self.client = Client(client_config)

        bot_config = BotConfig()
        self.bot = Bot(self.client, bot_config)

        self.bot_plugin = DiscordBotPlugin(self, self.bot, bot_config)
        self.bot.add_plugin(self.bot_plugin)

        self._children = {}
        self.message_queue = queue.Queue()
        self.webhooks = {}
        self._message_thread = None

    @staticmethod
    def is_nick(s, nicklen=None):
        return True

    def is_channel(self, s):
        """Returns whether the target is a channel."""
        try:
            chan = int(s)
        except (TypeError, ValueError):
            return False
        return chan in self.bot_plugin.state.channels

    def is_server_name(self, s):
        """Returns whether the string given is a valid IRC server name."""
        return s in self.bot_plugin.state.guilds

    def is_internal_client(self, uid):
        """Returns whether the given client is an internal one."""
        if uid == self.bot_plugin.me.id:
            return True
        return super().is_internal_client(uid)

    def get_friendly_name(self, entityid, caller=None):
        """
        Returns the friendly name of a SID (the guild name), UID (the nick), or channel (the name).
        """
        # internal PUID, handle appropriately
        if isinstance(entityid, str) and '@' in entityid:
            if entityid in self.users:
                return self.users[entityid].nick
            elif caller and entityid in caller.users:
                return caller.users[entityid].nick
            else:
                # Probably a server
                return entityid.split('@', 1)[0]

        if self.is_channel(entityid):
            return str(self.bot_plugin.state.channels[entityid])
        elif entityid in self.bot_plugin.state.users:
            return self.bot_plugin.state.users[entityid].username
        elif self.is_server_name(entityid):
            return self.bot_plugin.state.guilds[entityid].name
        else:
            raise KeyError("Unknown entity ID %s" % str(entityid))

    @staticmethod
    def wrap_message(source, target, text):
        """
        STUB: returns the message text wrapped onto multiple lines.
        """
        return [text]

    def _get_webhook(self, channel):
        """
        Returns the webhook saved for the given channel, or try to create one if none exists.
        """
        if channel.id in self.webhooks:  # We've already saved this webhook
            wh = self.webhooks[channel.id]
            log.debug('discord: Using saved webhook %s (%s) for channel %s', wh.id, wh.name, channel)
            return wh

        # Generate a webhook name based off a configurable prefix and the channel ID
        webhook_name = '%s-%d' % (self.serverdata.get('webhook_name') or 'PyLinkRelay', channel.id)

        for wh in channel.get_webhooks():
            if wh.name == webhook_name:  # This hook matches our name
                self.webhooks[channel.id] = wh
                log.info('discord: Using existing webhook %s (%s) for channel %s', wh.id, webhook_name, channel)
                return wh

        # If we didn't find any webhooks, create a new one
        wh = self.webhooks[channel.id] = channel.create_webhook(name=webhook_name)
        log.info('discord: Created new webhook %s (%s) for channel %s', wh.id, wh.name, channel)
        return wh

    def _get_webhook_fields(self, user):
        """
        Returns a dict of Relay substitution fields for the given User object.
        This attempts to find the original user via Relay if the .remote metadata field is set.

        The output includes all keys provided in User.get_fields(), plus the following:
            netname: The full network name of the network 'user' belongs to
            nettag: The short network tag of the network 'user' belongs to
            avatar: The URL to the user's avatar (str), or None if no avatar is specified
        """
        # Try to lookup the remote user data via relay metadata
        if hasattr(user, 'remote'):
            remotenet, remoteuid = user.remote
            try:
                netobj = world.networkobjects[remotenet]
                user = netobj.users[remoteuid]
            except LookupError:
                netobj = user._irc

        fields = user.get_fields()
        fields['netname'] = netobj.get_full_network_name()
        fields['nettag'] = netobj.name

        default_avatar_url = self.serverdata.get('default_avatar_url')
        avatar = None
        # XXX: we'll have a more rigorous matching system later on
        if user.services_account in self.serverdata.get('avatars', {}):
            avatar_url = self.serverdata['avatars'][user.services_account]
            p = urllib.parse.urlparse(avatar_url)
            log.debug('(%s) Got raw avatar URL %s for user %s', self.name, avatar_url, user)

            if p.scheme == 'gravatar' and libgravatar:  # gravatar:[email protected]
                try:
                    g = libgravatar.Gravatar(p.path)
                    log.debug('(%s) Using Gravatar email %s for user %s', self.name, p.path, user)
                    avatar = g.get_image(use_ssl=True)
                except:
                    log.exception('Failed to obtain Gravatar image for user %s/%s', user, p.path)

            elif p.scheme in ('http', 'https'):  # a direct image link
                avatar = avatar_url

            else:
                log.warning('(%s) Unknown avatar URI %s for user %s', self.name, avatar_url, user)
        elif default_avatar_url:
            log.debug('(%s) Avatar not defined for user %s; using default avatar %s', self.name, user, default_avatar_url)
            avatar = default_avatar_url
        else:
            log.debug('(%s) Avatar not defined for user %s; using default webhook avatar', self.name, user)
        fields['avatar'] = avatar
        return fields

    MAX_MESSAGE_SIZE = 2000
    def _message_builder(self):
        """
        Discord message queue handler. Also supports virtual users via webhooks.
        """
        def _send(sender, channel, pylink_target, message_parts):
            """
            Wrapper to send a joined message.
            """
            text = '\n'.join(message_parts)

            # Handle the case when the sender is not the PyLink client (sender != None)
            # For channels, use either virtual webhook users or CLIENTBOT_MESSAGE forwarding (relay_clientbot).
            if sender:
                user_fields = self._get_webhook_fields(sender)

                if channel.guild:  # This message belongs to a channel
                    netobj = self._children[channel.guild.id]

                    # Note: skip webhook sending for messages that contain only spaces, as that fails with
                    # 50006 "Cannot send an empty message" errors
                    if netobj.serverdata.get('use_webhooks') and text.strip():
                        user_format = netobj.serverdata.get('webhook_user_format', "$nick @ $netname")
                        tmpl = string.Template(user_format)
                        webhook_fake_username = tmpl.safe_substitute(self._get_webhook_fields(sender))

                        try:
                            webhook = self._get_webhook(channel)
                            webhook.execute(content=text[:self.MAX_MESSAGE_SIZE], username=webhook_fake_username, avatar_url=user_fields['avatar'])
                        except APIException as e:
                            if e.code == 10015 and channel.id in self.webhooks:
                                log.info("(%s) Invalidating webhook %s for channel %s due to Unknown Webhook error (10015)",
                                         self.name, self.webhooks[channel.id], channel)
                                del self.webhooks[channel.id]
                            elif e.code == 50013:
                                # Prevent spamming errors: disable webhooks we don't have the right permissions
                                log.warning("(%s) Disabling webhooks on guild %s/%s due to insufficient permissions (50013). Rehash to re-enable.",
                                            self.name, channel.guild.id, channel.guild.name)
                                self.serverdata.update(
                                    {'guilds':
                                        {channel.guild.id:
                                            {'use_webhooks': False}
                                        }
                                    })
                            else:
                                log.error("(%s) Caught API exception when sending webhook message to channel %s: %s/%s", self.name, channel, e.response.status_code, e.code)
                            log.debug("(%s) APIException full traceback:", self.name, exc_info=True)

                        except:
                            log.exception("(%s) Failed to send webhook message to channel %s", self.name, channel)
                        else:
                            return

                    for line in message_parts:
                        netobj.call_hooks([sender.uid, 'CLIENTBOT_MESSAGE', {'target': pylink_target, 'text': line}])
                    return
                else:
                    # This is a forwarded PM - prefix the message with its sender info.
                    pm_format = self.serverdata.get('pm_format', "Message from $nick @ $netname: $text")
                    user_fields['text'] = text
                    text = string.Template(pm_format).safe_substitute(user_fields)

            try:
                channel.send_message(text[:self.MAX_MESSAGE_SIZE])
            except Exception as e:
                log.exception("(%s) Could not send message to channel %s (pylink_target=%s)", self.name, channel, pylink_target)

        joined_messages = collections.defaultdict(collections.deque)
        while not self._aborted.is_set():
            try:
                # message is an instance of QueuedMessage (defined in this file)
                message = self.message_queue.get(timeout=BATCH_DELAY)
                message.text = utils.strip_irc_formatting(message.text)

                if not self.serverdata.get('allow_mention_everyone', False):
                    message.text = message.text.replace('@here', '@ here')
                    message.text = message.text.replace('@everyone', '@ everyone')

                # First, buffer messages by channel
                joined_messages[message.channel].append(message)

            except queue.Empty:  # Then process them together when we run out of things in the queue
                for channel, messages in joined_messages.items():
                    next_message = []
                    length = 0
                    current_sender = None
                    # We group messages here to avoid being throttled as often. In short, we want to send a message when:
                    # 1) The virtual sender (for webhook purposes) changes
                    # 2) We reach the message limit for one batch (2000 chars)
                    # 3) We run out of messages at the end
                    while messages:
                        message = messages.popleft()
                        next_message.append(message.text)
                        length += len(message.text)

                        if message.sender != current_sender or length >= self.MAX_MESSAGE_SIZE:
                            current_sender = message.sender
                            _send(current_sender, channel, message.pylink_target, next_message)
                            next_message.clear()
                            length = 0

                    # The last batch
                    if next_message:
                        _send(current_sender, channel, message.pylink_target, next_message)

                joined_messages.clear()
            except Exception:
                log.exception("Exception in message queueing thread:")

    def _create_child(self, server_id, guild_name):
        """
        Creates a virtual network object for a server with the given name.
        """
        # This is a bit different because we let the child server find its own name
        # and report back to us.
        child = DiscordServer(None, self, server_id, guild_name)
        world.networkobjects[child.name] = self._children[server_id] = child
        return child

    def _remove_child(self, server_id):
        """
        Removes a virtual network object with the given name.
        """
        pylink_netobj = self._children[server_id]
        pylink_netobj.call_hooks([None, 'PYLINK_DISCONNECT', {}])
        del self._children[server_id]
        del world.networkobjects[pylink_netobj.name]

    def connect(self):
        self._aborted.clear()
        self._message_thread = threading.Thread(name="Messaging thread for %s" % self.name,
                                                target=self._message_builder, daemon=True)
        self._message_thread.start()
        self.client.run()

    def disconnect(self):
        """Disconnects from Discord and shuts down this network object."""
        self._aborted.set()

        self._pre_disconnect()

        children = self._children.copy()
        for child in children:
            self._remove_child(child)

        self.client.gw.shutting_down = True
        self.client.gw.ws.close()

        self._post_disconnect()
Exemple #16
0
class PyLinkDiscordProtocol(PyLinkNetworkCoreWithUtils):
    S2S_BUFSIZE = 0

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if 'token' not in self.serverdata:
            raise ProtocolError("No API token defined under server settings")
        self.client_config = ClientConfig({'token': self.serverdata['token']})
        self.client = Client(self.client_config)
        self.bot_config = BotConfig()
        self.bot = Bot(self.client, self.bot_config)
        self.bot_plugin = DiscordBotPlugin(self, self.bot, self.bot_config)
        self.bot.add_plugin(self.bot_plugin)
        #setup_logging(level='DEBUG')
        self._children = {}
        self.message_queue = queue.Queue()

    @staticmethod
    def is_nick(s, nicklen=None):
        return True

    def is_channel(self, s):
        """Returns whether the target is a channel."""
        try:
            chan = int(s)
        except (TypeError, ValueError):
            return False
        return chan in self.bot_plugin.state.channels

    def is_server_name(self, s):
        """Returns whether the string given is a valid IRC server name."""
        return s in self.bot_plugin.state.guilds

    def get_friendly_name(self, entityid, caller=None):
        """
        Returns the friendly name of a SID (the guild name), UID (the nick), or channel (the name).
        """
        # internal PUID, handle appropriately
        if isinstance(entityid, str) and '@' in entityid:
            if entityid in self.users:
                return self.users[entityid].nick
            elif caller and entityid in caller.users:
                return caller.users[entityid].nick
            else:
                # Probably a server
                return entityid.split('@', 1)[0]

        if self.is_channel(entityid):
            return str(self.bot_plugin.state.channels[entityid])
        elif entityid in self.bot_plugin.state.users:
            return self.bot_plugin.state.users[entityid].username
        elif self.is_server_name(entityid):
            return self.bot_plugin.state.guilds[entityid].name
        else:
            raise KeyError("Unknown entity ID %s" % str(entityid))

    @staticmethod
    def wrap_message(source, target, text):
        """
        STUB: returns the message text wrapped onto multiple lines.
        """
        return [text]

    def _message_builder(self):
        current_channel_senders = {}
        joined_messages = collections.defaultdict(dict)
        while not self._aborted.is_set():
            try:
                message = self.message_queue.get(timeout=BATCH_DELAY)
                message_text = message.pop('text', '')
                channel = message.pop('target')
                current_sender = current_channel_senders.get(channel, None)

                # We'll enable this when we work on webhook support again...
                #if current_sender != message['sender']:
                #    self.flush(channel, joined_messages[channel])
                #    joined_messages[channel] = message

                current_channel_senders[channel] = message['sender']

                joined_message = joined_messages[channel].get('text', '')
                joined_messages[channel][
                    'text'] = joined_message + "\n{}".format(message_text)
            except queue.Empty:
                for channel, message_info in joined_messages.items():
                    self.flush(channel, message_info)
                joined_messages.clear()
                current_channel_senders.clear()

    def flush(self, channel, message_info):
        message_text = message_info.pop('text', '').strip()
        if message_text:
            if message_info.get('username'):
                message_info['webhook'].execute(
                    content=message_text,
                    username=message_info['username'],
                    avatar_url=message_info.get('avatar'),
                )
            else:
                channel.send_message(message_text)

    def _create_child(self, server_id, guild_name):
        """
        Creates a virtual network object for a server with the given name.
        """
        # Try to find a predefined server name; if that fails, use the server id.
        # We don't use the guild name here because those can be changed at any time,
        # confusing plugins that store data by PyLink network names.
        fallback_name = 'd%d' % server_id
        name = self.serverdata.get('server_names',
                                   {}).get(server_id, fallback_name)

        if name in world.networkobjects:
            raise ValueError("Attempting to reintroduce network with name %r" %
                             name)
        child = DiscordServer(name, self, server_id, guild_name)
        world.networkobjects[name] = self._children[server_id] = child
        return child

    def _remove_child(self, server_id):
        """
        Removes a virtual network object with the given name.
        """
        pylink_netobj = self._children[server_id]
        pylink_netobj.call_hooks([None, 'PYLINK_DISCONNECT', {}])
        del self._children[server_id]
        del world.networkobjects[pylink_netobj.name]

    def connect(self):
        self._aborted.clear()
        self.client.run()

    def disconnect(self):
        """Disconnects from Discord and shuts down this network object."""
        self._aborted.set()

        self._pre_disconnect()

        children = self._children.copy()
        for child in children:
            self._remove_child(child)

        self.client.gw.shutting_down = True
        self.client.gw.ws.close()

        self._post_disconnect()
Exemple #17
0
from gevent.monkey import patch_all; patch_all()

import logging
from os import environ

from disco.bot import Bot, BotConfig
from disco.client import Client, ClientConfig
from disco.util.logging import setup_logging


setup_logging(level=logging.INFO)
config = ClientConfig.from_file("config.json")
config.token = environ['token']
client = Client(config)
bot_config = BotConfig(config.bot)
bot = Bot(client, bot_config)

if __name__ == '__main__':
    bot.run_forever()
Exemple #18
0
def disco_main(run=False):
    """
    Creates an argument parser and parses a standard set of command line arguments,
    creating a new :class:`Client`.

    Returns
    -------
    :class:`Client`
        A new Client from the provided command line arguments
    """
    args = parser.parse_args()

    from disco.client import Client, ClientConfig
    from disco.bot import Bot, BotConfig
    from disco.util.token import is_valid_token
    from disco.util.logging import setup_logging

    if os.path.exists(args.config):
        config = ClientConfig.from_file(args.config)
    else:
        config = ClientConfig()

    config.manhole_enable = args.manhole
    if args.manhole_bind:
        config.manhole_bind = args.manhole_bind

    for k, v in six.iteritems(vars(args)):
        if hasattr(config, k) and v is not None:
            setattr(config, k, v)

    if not is_valid_token(config.token):
        print('Invalid token passed')
        return

    if args.shard_auto:
        from disco.gateway.sharder import AutoSharder
        AutoSharder(config).run()
        return

    # TODO: make configurable
    setup_logging(level=getattr(logging, args.log_level.upper()))

    client = Client(config)

    bot = None
    if args.run_bot or hasattr(config, 'bot'):
        bot_config = BotConfig(config.bot) if hasattr(config,
                                                      'bot') else BotConfig()
        if not hasattr(bot_config, 'plugins'):
            bot_config.plugins = args.plugin
        else:
            bot_config.plugins += args.plugin

        if args.http_bind:
            bot_config.http_enabled = True
            host, port = args.http_bind.split(':', 1)
            bot_config.http_host = host
            bot_config.http_port = int(port)

        bot = Bot(client, bot_config)

    if run:
        (bot or client).run_forever()

    return (bot or client)
Exemple #19
0
class Program(dogma.program.Program):
    def __init__(self, agent):
        super().__init__(agent)
        self.client = None
        self.bot = None
        self.me = None

    def load(self, config=None, state=None):
        super().load(config=config, state=state)

        conf = ClientConfig()
        conf.token = self.config.get('token')
        conf.max_reconnects = 0
        self.client = Client(conf)
        self.client.parent = self
        self.client.agent = self.agent

        self.bot = None
        bot_config = BotConfig()
        bot_config.commands_require_mention = False
        bot_config.commands_prefix = self.config.get('command_prefix', '.')

        bot_config.levels = self.config.get('access', {})

        bot_config.commands_level_getter = self.level_getter
        self.bot = Bot(self.client, bot_config)

        for unique_id, pconf in self.config.get('plugins').items():
            if pconf is None:
                pconf = {}

            pconf.setdefault("_autoload", True)
            pconf.setdefault("_module", unique_id)
            if not pconf["_autoload"]:
                continue

            self.bot.add_plugin_module(pconf["_module"], pconf)  #TODO: replace

        for _, plugin in self.bot.plugins.items():
            plugin.parent = self

        self.bot.agent = self.agent
        self.bot.parent = self

        self.me = self.client.api.users_me_get()

    def unload(self, state=None):
        state = super().unload(state)
        return state

    def run(self):
        (self.bot or self.client).run_forever()

    def notify(self, title, content):
        if "owner" in self.config:
            return

        user = self.client.api.users_get(self.config["owner"])
        if user:
            user.open_dm().send_message("%s : %s" % (title, content))

    def is_me(self, actor):
        return (actor.id == self.me.id)

    def is_owner(self, actor):
        return ('owner' in self.config and actor.id == self.config['owner'])

    @staticmethod
    def level_getter(client, actor):
        # replacement for disco.py's access level check. limited since its only passing 'actor'
        # only need this for the bot's .commands anyways. Ideally should eventually replace disco's
        # .command system entirely
        level = client.config.levels.get(str(actor.id), 0)
        return level

    @staticmethod
    def check_access(plugin,
                     event,
                     level=0,
                     dm_level=0,
                     allow_dm=True,
                     allow_bots=False,
                     require_whitelists=False):
        # this should use a role based system instead (note: custom defined roles, not discord server roles)
        if plugin.parent.is_me(event.author):  # ignore ourself
            return False

        if (not allow_bots and event.author.bot) or not event.content:
            return False

        # seems weird to get access this way, since its calling the static method level_getter above.
        access = plugin.bot.get_level(event.author)

        logger.debug("Checking access level %s >= %s for %s", access, level,
                     str(plugin))

        if event.guild:
            logger.debug("Channel: %s", event.channel.id)
            if access < level:
                return False

            config = plugin.config if plugin.config else {}
            server_whitelist = config.get('server_whitelist', [])
            channel_whitelist = config.get('channel_whitelist', [])
            channel_blacklist = config.get('channel_blacklist', [])

            # check channel blacklists
            if event.channel.id in channel_blacklist:
                return False

            if require_whitelists and (event.guild.id not in server_whitelist
                                       and event.channel.id
                                       not in channel_whitelist):
                return False
            logger.debug("allowing channel access")
            return True

        if plugin.parent.is_owner(event.author):
            logger.debug("allowing owner DM access")
            return True

        logger.debug("DM access result: %s",
                     (allow_dm is True and access < level))

        return (allow_dm is True and access < level)
Exemple #20
0
class PyLinkDiscordProtocol(PyLinkNetworkCoreWithUtils):

    def __init__(self, *args, **kwargs):
        from gevent import monkey
        monkey.patch_all()
        super().__init__(*args, **kwargs)
        self._hooks_queue = queue.Queue()

        if 'token' not in self.serverdata:
            raise ProtocolError("No API token defined under server settings")
        self.client_config = ClientConfig({'token': self.serverdata['token']})
        self.client = Client(self.client_config)
        self.bot_config = BotConfig()
        self.bot = Bot(self.client, self.bot_config)
        self.bot_plugin = DiscordBotPlugin(self, self.bot, self.bot_config)
        self.bot.add_plugin(self.bot_plugin)
        setup_logging(level='DEBUG')
        self._children = {}
        self.message_queue = queue.Queue()

    def _message_builder(self):
        current_channel_senders = {}
        joined_messages = defaultdict(dict)
        while not self._aborted.is_set():
            try:
                message = self.message_queue.get(timeout=0.1)
                message_text = message.pop('text', '')
                channel = message.pop('target')
                current_sender = current_channel_senders.get(channel, None)

                if current_sender != message['sender']:
                    self.flush(channel, joined_messages[channel])
                    joined_messages[channel] = message

                current_channel_senders[channel] = message['sender']

                joined_message = joined_messages[channel].get('text', '')
                joined_messages[channel]['text'] = joined_message + "\n{}".format(message_text)
            except queue.Empty:
                for channel, message_info in joined_messages.items():
                    self.flush(channel, message_info)
                joined_messages = defaultdict(dict)
                current_channel_senders = {}

    def flush(self, channel, message_info):
        message_text = message_info.pop('text', '').strip()
        if message_text:
            if message_info.get('username'):
                message_info['webhook'].execute(
                    content=message_text,
                    username=message_info['username'],
                    avatar_url=message_info.get('avatar'),
                    )
            else:
                channel.send_message(message_text)


    def _process_hooks(self):
        """Loop to process incoming hook data."""
        while not self._aborted.is_set():
            data = self._hooks_queue.get()
            if data is None:
                log.debug('(%s) Stopping queue thread due to getting None as item', self.name)
                break
            elif self not in world.networkobjects.values():
                log.debug('(%s) Stopping stale queue thread; no longer matches world.networkobjects', self.name)
                break

            subserver, data = data
            if subserver not in world.networkobjects:
                log.error('(%s) Not queuing hook for subserver %r no longer in networks list.',
                          self.name, subserver)
            elif subserver in self._children:
                self._children[subserver].call_hooks(data)

    def _add_hook(self, subserver, data):
        """
        Pushes a hook payload for the given subserver.
        """
        if subserver not in self._children:
            raise ValueError("Unknown subserver %s" % subserver)
        self._hooks_queue.put_nowait((
            subserver,
            data
        ))

    def _create_child(self, name, server_id):
        """
        Creates a virtual network object for a server with the given name.
        """
        if name in world.networkobjects:
            raise ValueError("Attempting to reintroduce network with name %r" % name)
        child = DiscordServer(name, self, server_id)
        world.networkobjects[name] = self._children[name] = child
        return child

    def _remove_child(self, name):
        """
        Removes a virtual network object with the given name.
        """
        self._add_hook(name, [None, 'PYLINK_DISCONNECT', {}])
        del self._children[name]
        del world.networkobjects[name]

    def connect(self):
        self._aborted.clear()

        self._queue_thread = threading.Thread(name="Queue thread for %s" % self.name,
                                             target=self._process_hooks, daemon=True)
        self._queue_thread.start()

        self._message_thread = threading.Thread(name="Message thread for %s" % self.name,
                                                target=self._message_builder, daemon=True)
        self._message_thread.start()

        self.client.run()

    def websocket_close(self, *_, **__):
        return self.disconnect()

    def disconnect(self):
        self._aborted.set()

        self._pre_disconnect()

        log.debug('(%s) Killing hooks handler', self.name)
        try:
            # XXX: queue.Queue.queue isn't actually documented, so this is probably not reliable in the long run.
            with self._hooks_queue.mutex:
                self._hooks_queue.queue[0] = None
        except IndexError:
            self._hooks_queue.put(None)

        children = self._children.copy()
        for child in children:
            self._remove_child(child)

        if world.shutting_down.is_set():
            self.bot.client.gw.shutting_down = True
        log.debug('(%s) Sending Discord logout', self.name)
        self.bot.client.gw.session_id = None
        self.bot.client.gw.ws.close()

        self._post_disconnect()
Exemple #21
0
def disco_main(run=False):
    """
    Creates an argument parser and parses a standard set of command line arguments,
    creating a new :class:`Client`.
    Returns
    -------
    :class:`Client`
        A new Client from the provided command line arguments
    """
    from disco.client import Client, ClientConfig
    from disco.bot import Bot, BotConfig
    from disco.util.logging import setup_logging, LOG_FORMAT

    from bot.base import bot

    args = bot.local.disco

    # Create the base configuration object
    if args.config:
        config = ClientConfig.from_file(args.config)
    else:
        config = ClientConfig(args.to_dict())

    for arg_key, config_key in six.iteritems(CONFIG_OVERRIDE_MAPPING):
        if getattr(args, arg_key) is not None:
            setattr(config, config_key, getattr(args, arg_key))

    # Setup the auto-sharder
    if args.shard_auto:
        from disco.gateway.sharder import AutoSharder
        AutoSharder(config).run()
        return

    # Setup logging based on the configured level

    if not os.path.exists("logs"):
        os.makedirs("logs")

    file_handler = logging.FileHandler("logs/bot.log")
    file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
    file_handler.setLevel(config.log_level.upper())
    stream_handler = logging.StreamHandler()
    setup_logging(
        handlers=(file_handler, stream_handler),
        level=getattr(logging, config.log_level.upper()),
    )

    # Build out client object
    client = Client(config)

    # If applicable, build the bot and load plugins
    bot = None
    if args.run_bot or hasattr(config, 'bot'):
        bot_config = BotConfig(config.bot.to_dict()) if hasattr(
            config, 'bot') else BotConfig()
        if not hasattr(bot_config, 'plugins'):
            bot_config.plugins = args.plugin
        else:
            bot_config.plugins += args.plugin

        bot = Bot(client, bot_config)

    if run:
        (bot or client).run_forever()

    return (bot or client)