PageRenderTime 45ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/irctk/bot.py

https://github.com/tarmack/irctk
Python | 222 lines | 131 code | 41 blank | 50 comment | 23 complexity | c4b795d2e8cbf5397854c59fcebb03cf MD5 | raw file
  1. '''
  2. irctk.bot
  3. ---------
  4. This defines the main class, `Bot`, for use in creating new IRC bot apps.
  5. '''
  6. import os
  7. import time
  8. import inspect
  9. import thread
  10. from .logging import create_logger
  11. from .config import Config
  12. from .reloader import ReloadHandler
  13. from .plugins import PluginHandler
  14. from .ircclient import TcpClient, IrcWrapper
  15. class Bot(object):
  16. _instance = None
  17. root_path = os.path.abspath('')
  18. logger = create_logger()
  19. default_config = dict({
  20. 'SERVER' : 'irc.voxinfinitus.net',
  21. 'PORT' : 6697,
  22. 'SSL' : True,
  23. 'TIMEOUT' : 300,
  24. 'NICK' : 'Kaa',
  25. 'REALNAME' : 'Kaa the rock python',
  26. 'CHANNELS' : ['#voxinfinitus'],
  27. 'PLUGINS' : [],
  28. 'EVENTS' : [],
  29. 'MAX_WORKERS' : 7,
  30. 'MIN_WORKERS' : 3,
  31. })
  32. def __init__(self):
  33. self.config = Config(self.root_path, self.default_config)
  34. self.plugin = PluginHandler(self.config, self.logger, self.reply)
  35. def __new__(cls, *args, **kwargs):
  36. '''Here we override the `__new__` method in order to achieve a
  37. singleton effect. In this way we can reload instances of the object
  38. without having to worry about which instance we reference.'''
  39. if not cls._instance:
  40. cls._instance = super(Bot, cls).__new__(cls, *args, **kwargs)
  41. return cls._instance
  42. def _create_connection(self):
  43. self.connection = TcpClient(
  44. self.config['SERVER'],
  45. self.config['PORT'],
  46. self.config['SSL'],
  47. self.config['TIMEOUT'],
  48. logger=self.logger,
  49. )
  50. self.irc = IrcWrapper(
  51. self.connection,
  52. self.config['NICK'],
  53. self.config['REALNAME'],
  54. self.config['CHANNELS'],
  55. self.logger
  56. )
  57. def _parse_input(self, prefix='.', wait=0.01):
  58. '''This internal method handles the parsing of commands and events.
  59. Hooks for commands are prefixed with a character, by default `.`. This
  60. may be overriden by specifying `prefix`.
  61. A context is maintained by our IRC wrapper, `IrcWrapper`; referenced
  62. as `self.irc`. In order to prevent a single line from being parsed
  63. repeatedly a variable `stale` is set to either True or False.
  64. If the context is fresh, i.e. not `stale`, we loop over the line
  65. looking for matches to plugins.
  66. Once the context is consumed, we set the context variable `stale` to
  67. True.
  68. '''
  69. while True:
  70. time.sleep(wait)
  71. with self.irc.lock:
  72. context_stale = lambda : self.irc.context.get('stale')
  73. args = self.irc.context.get('args')
  74. command = self.irc.context.get('command')
  75. message = self.irc.context.get('message')
  76. while not context_stale() and args:
  77. if message.startswith(prefix):
  78. for plugin in self.config['PLUGINS']:
  79. plugin['context'] = dict(self.irc.context)
  80. hook = prefix + plugin['hook']
  81. try:
  82. self.plugin.enqueue_plugin(plugin, hook, message)
  83. except Exception, e:
  84. self.logger.error(str(e))
  85. continue
  86. if command and command.isupper():
  87. for event in self.config['EVENTS']:
  88. event['context'] = dict(self.irc.context)
  89. hook = event['hook']
  90. try:
  91. self.plugin.enqueue_plugin(event, hook, command)
  92. except Exception, e:
  93. self.logger.error(str(e))
  94. continue
  95. # we're done here, context is stale, give us fresh fruit!
  96. self.irc.context['stale'] = True
  97. def command(self, hook=None, **kwargs):
  98. '''This method provides a decorator that can be used to load a
  99. function into the global plugins list.:
  100. If the `hook` parameter is provided the decorator will assign the hook
  101. key to the value of `hook`, update the `plugin` dict, and then return
  102. the wrapped function to the wrapper.
  103. Therein the plugin dictionary is updated with the `func` key whose
  104. value is set to the wrapped function.
  105. Otherwise if no `hook` parameter is passed the, `hook` is assumed to
  106. be the wrapped function and handled accordingly.
  107. '''
  108. plugin = {}
  109. def wrapper(func):
  110. plugin.setdefault('hook', func.func_name)
  111. plugin['funcs'] = [func]
  112. self.plugin.update_plugins(plugin, 'PLUGINS')
  113. return func
  114. if kwargs or not inspect.isfunction(hook):
  115. if hook:
  116. plugin['hook'] = hook
  117. plugin.update(kwargs)
  118. return wrapper
  119. else:
  120. return wrapper(hook)
  121. def event(self, hook, **kwargs):
  122. '''This method provides a decorator that can be used to load a
  123. function into the global events list.
  124. It assumes one parameter, `hook`, i.e. the event you wish to bind
  125. this wrapped function to. For example, JOIN, which would call the
  126. function on all JOIN events.
  127. '''
  128. plugin = {}
  129. def wrapper(func):
  130. plugin['funcs'] = [func]
  131. self.plugin.update_plugins(plugin, 'EVENTS')
  132. return func
  133. plugin['hook'] = hook
  134. plugin.update(kwargs)
  135. return wrapper
  136. def add_command(self, hook, func):
  137. '''TODO'''
  138. self.plugin.add_plugin(hook, func, command=True)
  139. def add_event(self, hook, func):
  140. '''TODO'''
  141. self.plugin.add_plugin(hook, func, event=True)
  142. def remove_command(self, hook, func):
  143. '''TODO'''
  144. self.plugin.remove_plugin(hook, func, command=True)
  145. def remove_event(self, hook, func):
  146. '''TODO'''
  147. self.plugin.remove_plugin(hook, func, event=True)
  148. def reply(self, message, context, action=False, notice=False, line_limit=400):
  149. '''TODO'''
  150. if context['sender'].startswith('#'):
  151. recipient = context['sender']
  152. else:
  153. recipient = context['user']
  154. messages = []
  155. def handle_long_message(message):
  156. #truncate_at = message.rfind(' ', 1, line_limit) + 1
  157. message, extra = message[:line_limit], message[line_limit:]
  158. messages.append(message)
  159. if extra:
  160. handle_long_message(extra)
  161. handle_long_message(message)
  162. for message in messages:
  163. self.irc.send_message(recipient, message, action, notice)
  164. def run(self, wait=0.01):
  165. self._create_connection() # updates to the latest config
  166. self.connection.connect()
  167. self.irc.run()
  168. thread.start_new_thread(self._parse_input, ())
  169. plugin_lists = [self.config['PLUGINS'], self.config['EVENTS']]
  170. self.reloader = ReloadHandler(plugin_lists, self.plugin, self.logger)
  171. while True:
  172. time.sleep(wait)