/irctk/bot.py
Python | 222 lines | 131 code | 41 blank | 50 comment | 23 complexity | c4b795d2e8cbf5397854c59fcebb03cf MD5 | raw file
- '''
- irctk.bot
- ---------
-
- This defines the main class, `Bot`, for use in creating new IRC bot apps.
- '''
- import os
- import time
- import inspect
- import thread
- from .logging import create_logger
- from .config import Config
- from .reloader import ReloadHandler
- from .plugins import PluginHandler
- from .ircclient import TcpClient, IrcWrapper
- class Bot(object):
- _instance = None
- root_path = os.path.abspath('')
- logger = create_logger()
-
- default_config = dict({
- 'SERVER' : 'irc.voxinfinitus.net',
- 'PORT' : 6697,
- 'SSL' : True,
- 'TIMEOUT' : 300,
- 'NICK' : 'Kaa',
- 'REALNAME' : 'Kaa the rock python',
- 'CHANNELS' : ['#voxinfinitus'],
- 'PLUGINS' : [],
- 'EVENTS' : [],
- 'MAX_WORKERS' : 7,
- 'MIN_WORKERS' : 3,
- })
-
- def __init__(self):
- self.config = Config(self.root_path, self.default_config)
- self.plugin = PluginHandler(self.config, self.logger, self.reply)
-
- def __new__(cls, *args, **kwargs):
- '''Here we override the `__new__` method in order to achieve a
- singleton effect. In this way we can reload instances of the object
- without having to worry about which instance we reference.'''
-
- if not cls._instance:
- cls._instance = super(Bot, cls).__new__(cls, *args, **kwargs)
- return cls._instance
-
- def _create_connection(self):
- self.connection = TcpClient(
- self.config['SERVER'],
- self.config['PORT'],
- self.config['SSL'],
- self.config['TIMEOUT'],
- logger=self.logger,
- )
-
- self.irc = IrcWrapper(
- self.connection,
- self.config['NICK'],
- self.config['REALNAME'],
- self.config['CHANNELS'],
- self.logger
- )
-
- def _parse_input(self, prefix='.', wait=0.01):
- '''This internal method handles the parsing of commands and events.
- Hooks for commands are prefixed with a character, by default `.`. This
- may be overriden by specifying `prefix`.
-
- A context is maintained by our IRC wrapper, `IrcWrapper`; referenced
- as `self.irc`. In order to prevent a single line from being parsed
- repeatedly a variable `stale` is set to either True or False.
-
- If the context is fresh, i.e. not `stale`, we loop over the line
- looking for matches to plugins.
-
- Once the context is consumed, we set the context variable `stale` to
- True.
- '''
-
- while True:
- time.sleep(wait)
-
- with self.irc.lock:
- context_stale = lambda : self.irc.context.get('stale')
- args = self.irc.context.get('args')
- command = self.irc.context.get('command')
- message = self.irc.context.get('message')
- while not context_stale() and args:
- if message.startswith(prefix):
- for plugin in self.config['PLUGINS']:
- plugin['context'] = dict(self.irc.context)
- hook = prefix + plugin['hook']
- try:
- self.plugin.enqueue_plugin(plugin, hook, message)
- except Exception, e:
- self.logger.error(str(e))
- continue
- if command and command.isupper():
- for event in self.config['EVENTS']:
- event['context'] = dict(self.irc.context)
- hook = event['hook']
- try:
- self.plugin.enqueue_plugin(event, hook, command)
- except Exception, e:
- self.logger.error(str(e))
- continue
-
- # we're done here, context is stale, give us fresh fruit!
- self.irc.context['stale'] = True
-
- def command(self, hook=None, **kwargs):
- '''This method provides a decorator that can be used to load a
- function into the global plugins list.:
-
- If the `hook` parameter is provided the decorator will assign the hook
- key to the value of `hook`, update the `plugin` dict, and then return
- the wrapped function to the wrapper.
-
- Therein the plugin dictionary is updated with the `func` key whose
- value is set to the wrapped function.
-
- Otherwise if no `hook` parameter is passed the, `hook` is assumed to
- be the wrapped function and handled accordingly.
- '''
-
- plugin = {}
-
- def wrapper(func):
- plugin.setdefault('hook', func.func_name)
- plugin['funcs'] = [func]
- self.plugin.update_plugins(plugin, 'PLUGINS')
- return func
-
- if kwargs or not inspect.isfunction(hook):
- if hook:
- plugin['hook'] = hook
- plugin.update(kwargs)
- return wrapper
- else:
- return wrapper(hook)
-
- def event(self, hook, **kwargs):
- '''This method provides a decorator that can be used to load a
- function into the global events list.
-
- It assumes one parameter, `hook`, i.e. the event you wish to bind
- this wrapped function to. For example, JOIN, which would call the
- function on all JOIN events.
- '''
-
- plugin = {}
-
- def wrapper(func):
- plugin['funcs'] = [func]
- self.plugin.update_plugins(plugin, 'EVENTS')
- return func
-
- plugin['hook'] = hook
- plugin.update(kwargs)
- return wrapper
-
- def add_command(self, hook, func):
- '''TODO'''
-
- self.plugin.add_plugin(hook, func, command=True)
-
- def add_event(self, hook, func):
- '''TODO'''
-
- self.plugin.add_plugin(hook, func, event=True)
-
- def remove_command(self, hook, func):
- '''TODO'''
-
- self.plugin.remove_plugin(hook, func, command=True)
-
- def remove_event(self, hook, func):
- '''TODO'''
-
- self.plugin.remove_plugin(hook, func, event=True)
-
- def reply(self, message, context, action=False, notice=False, line_limit=400):
- '''TODO'''
-
- if context['sender'].startswith('#'):
- recipient = context['sender']
- else:
- recipient = context['user']
-
- messages = []
-
- def handle_long_message(message):
- #truncate_at = message.rfind(' ', 1, line_limit) + 1
- message, extra = message[:line_limit], message[line_limit:]
- messages.append(message)
- if extra:
- handle_long_message(extra)
-
- handle_long_message(message)
-
- for message in messages:
- self.irc.send_message(recipient, message, action, notice)
-
- def run(self, wait=0.01):
- self._create_connection() # updates to the latest config
-
- self.connection.connect()
- self.irc.run()
-
- thread.start_new_thread(self._parse_input, ())
-
- plugin_lists = [self.config['PLUGINS'], self.config['EVENTS']]
- self.reloader = ReloadHandler(plugin_lists, self.plugin, self.logger)
-
- while True:
- time.sleep(wait)