Backport part of v4 Bot class

This commit is contained in:
nemunaire 2014-08-29 11:53:32 +02:00
parent a8fe4c5159
commit 8d1919a36b
3 changed files with 164 additions and 88 deletions

226
bot.py
View File

@ -20,12 +20,16 @@ from datetime import datetime
from datetime import timedelta from datetime import timedelta
import logging import logging
from queue import Queue from queue import Queue
import re
import threading import threading
import time import time
import re import uuid
import consumer __version__ = '3.4.dev0'
import event __author__ = 'nemunaire'
from consumer import Consumer, EventConsumer, MessageConsumer
from event import ModuleEvent
import hooks import hooks
from networkbot import NetworkBot from networkbot import NetworkBot
from server.IRC import IRCServer from server.IRC import IRCServer
@ -34,18 +38,29 @@ import response
logger = logging.getLogger("nemubot.bot") logger = logging.getLogger("nemubot.bot")
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
class Bot: class Bot:
def __init__(self, ip, realname, mp=list()):
# Bot general informations """Class containing the bot context and ensuring key goals"""
self.version = 3.4
self.version_txt = "3.4-dev" def __init__(self, ip="127.0.0.1", modules_paths=list(), data_path="./datas/"):
logger.info("Initiate nemubot v%s", self.version_txt) """Initialize the bot context
Keyword arguments:
ip -- The external IP of the bot (default: 127.0.0.1)
modules_paths -- Paths to all directories where looking for module
data_path -- Path to directory where store bot context data
"""
logger.info("Initiate nemubot v%s", __version__)
# External IP for accessing this bot
self.ip = ip
# Context paths
self.modules_paths = modules_paths
self.data_path = data_path
# Save various informations # Save various informations
self.ip = ip
self.realname = realname
self.ctcp_capabilities = dict() self.ctcp_capabilities = dict()
self.init_ctcp_capabilities() self.init_ctcp_capabilities()
@ -54,10 +69,6 @@ class Bot:
self.modules = dict() self.modules = dict()
self.modules_configuration = dict() self.modules_configuration = dict()
# Context paths
self.modules_path = mp
self.datas_path = './datas/'
# Events # Events
self.events = list() self.events = list()
self.event_timer = None self.event_timer = None
@ -85,13 +96,13 @@ class Bot:
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
self.ctcp_capabilities["DCC"] = self._ctcp_dcc self.ctcp_capabilities["DCC"] = self._ctcp_dcc
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response( self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
msg.sender, "NEMUBOT %f" % self.version) msg.sender, "NEMUBOT %s" % __version__)
self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response( self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response(
msg.sender, "TIME %s" % (datetime.now())) msg.sender, "TIME %s" % (datetime.now()))
self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response( self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response(
msg.sender, "USERINFO %s" % self.realname) msg.sender, "USERINFO %s" % srv.realname)
self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response(
msg.sender, "VERSION nemubot v%s" % self.version_txt) msg.sender, "VERSION nemubot v%s" % __version__)
logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
def _ctcp_clientinfo(self, srv, msg): def _ctcp_clientinfo(self, srv, msg):
@ -110,43 +121,85 @@ class Bot:
logger.error("DCC: unable to connect to %s:%s", ip, msg.cmds[4]) logger.error("DCC: unable to connect to %s:%s", ip, msg.cmds[4])
# Events methods
def add_event(self, evt, eid=None, module_src=None): def add_event(self, evt, eid=None, module_src=None):
"""Register an event and return its identifiant for futur update""" """Register an event and return its identifiant for futur update
Return:
None if the event is not in the queue (eg. if it has been executed during the call) or
returns the event ID.
Argument:
evt -- The event object to add
Keyword arguments:
eid -- The desired event ID (object or string UUID)
module_src -- The module to which the event is attached to
"""
# Generate the event id if no given
if eid is None: if eid is None:
# Find an ID eid = uuid.uuid1()
now = datetime.now()
evt.id = "%d%c%d%d%c%d%d%c%d" % (now.year, ID_letters[now.microsecond % 52], # Fill the id field of the event
now.month, now.day, ID_letters[now.microsecond % 42], if type(eid) is uuid.UUID:
now.hour, now.minute, ID_letters[now.microsecond % 32], evt.id = str(eid)
now.second)
else: else:
# Ok, this is quite useless...
try:
evt.id = str(uuid.UUID(eid))
except ValueError:
evt.id = eid evt.id = eid
# Add the event in place # Add the event in its place
t = evt.current t = evt.current
i = -1 i = 0 # sentinel
for i in range(0, len(self.events)): for i in range(0, len(self.events)):
if self.events[i].current > t: if self.events[i].current > t:
i -= 1
break break
self.events.insert(i + 1, evt) self.events.insert(i, evt)
if i == -1:
self.update_timer() if i == 0:
if len(self.events) <= 0 or self.events[i+1] != evt: # First event changed, reset timer
self._update_event_timer()
if len(self.events) <= 0 or self.events[i] != evt:
# Our event has been executed and removed from queue
return None return None
# Register the event in the source module
if module_src is not None: if module_src is not None:
module_src.REGISTERED_EVENTS.append(evt.id) module_src.REGISTERED_EVENTS.append(evt.id)
evt.module_src = module_src
logger.info("New event registered: %s -> %s", evt.id, evt) logger.info("New event registered: %s -> %s", evt.id, evt)
return evt.id return evt.id
def del_event(self, id, module_src=None):
"""Find and remove an event from list""" def del_event(self, evt, module_src=None):
logger.info("Removing event: %s from %s", id, module_src) """Find and remove an event from list
Return:
True if the event has been found and removed, False else
Argument:
evt -- The ModuleEvent object to remove or just the event identifier
Keyword arguments:
module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent)
"""
logger.info("Removing event: %s from %s", evt, module_src)
if type(evt) is ModuleEvent:
id = evt.id
module_src = evt.module_src
else:
id = evt
if len(self.events) > 0 and id == self.events[0].id: if len(self.events) > 0 and id == self.events[0].id:
self.events.remove(self.events[0]) self.events.remove(self.events[0])
self.update_timer() self._update_event_timer()
if module_src is not None: if module_src is not None:
module_src.REGISTERED_EVENTS.remove(evt.id) module_src.REGISTERED_EVENTS.remove(evt.id)
return True return True
@ -160,35 +213,52 @@ class Bot:
return True return True
return False return False
def update_timer(self):
"""Relaunch the timer to end with the closest event""" def _update_event_timer(self):
"""(Re)launch the timer to end with the closest event"""
# Reset the timer if this is the first item # Reset the timer if this is the first item
if self.event_timer is not None: if self.event_timer is not None:
self.event_timer.cancel() self.event_timer.cancel()
if len(self.events) > 0: if len(self.events) > 0:
logger.debug("Update timer: next event in %d seconds", logger.debug("Update timer: next event in %d seconds",
self.events[0].time_left.seconds) self.events[0].time_left.seconds)
if datetime.now() + timedelta(seconds=5) >= self.events[0].current: if datetime.now() + timedelta(seconds=5) >= self.events[0].current:
while datetime.now() < self.events[0].current: while datetime.now() < self.events[0].current:
time.sleep(0.6) time.sleep(0.6)
self.end_timer() self._end_event_timer()
else: else:
self.event_timer = threading.Timer( self.event_timer = threading.Timer(
self.events[0].time_left.seconds + 1, self.end_timer) self.events[0].time_left.seconds + 1, self._end_event_timer)
self.event_timer.start() self.event_timer.start()
else: else:
logger.debug("Update timer: no timer left") logger.debug("Update timer: no timer left")
def end_timer(self):
"""Function called at the end of the timer"""
#print ("end timer")
while len(self.events) > 0 and datetime.now() >= self.events[0].current:
#print ("end timer: while")
evt = self.events.pop(0)
self.cnsr_queue.put_nowait(consumer.EventConsumer(evt))
self.update_consumers()
self.update_timer() def _end_event_timer(self):
"""Function called at the end of the event timer"""
while len(self.events) > 0 and datetime.now() >= self.events[0].current:
evt = self.events.pop(0)
self.cnsr_queue.put_nowait(EventConsumer(evt))
self._launch_consumers()
self._update_event_timer()
# Consumers methods
def _launch_consumers(self):
"""Launch new consumer threads if necessary"""
while self.cnsr_queue.qsize() > self.cnsr_thrd_size:
# Next launch if two more items in queue
self.cnsr_thrd_size += 2
c = Consumer(self)
self.cnsr_thrd.append(c)
c.start()
def add_server(self, node, nick, owner, realname, ssl=False): def add_server(self, node, nick, owner, realname, ssl=False):
@ -207,6 +277,21 @@ class Bot:
return False return False
# Modules methods
def add_modules_path(self, path):
"""Add a path to the modules_path array, used by module loader"""
# The path must end by / char
if path[len(path)-1] != "/":
path = path + "/"
if path not in self.modules_paths:
self.modules_paths.append(path)
return True
return False
def add_module(self, module): def add_module(self, module):
"""Add a module to the context, if already exists, unload the """Add a module to the context, if already exists, unload the
old one before""" old one before"""
@ -220,19 +305,6 @@ class Bot:
return True return True
def add_modules_path(self, path):
"""Add a path to the modules_path array, used by module loader"""
# The path must end by / char
if path[len(path)-1] != "/":
path = path + "/"
if path not in self.modules_path:
self.modules_path.append(path)
return True
return False
def unload_module(self, name): def unload_module(self, name):
"""Unload a module""" """Unload a module"""
if name in self.modules: if name in self.modules:
@ -252,22 +324,14 @@ class Bot:
return True return True
return False return False
def update_consumers(self):
"""Launch new consumer thread if necessary"""
if self.cnsr_queue.qsize() > self.cnsr_thrd_size:
c = consumer.Consumer(self)
self.cnsr_thrd.append(c)
c.start()
self.cnsr_thrd_size += 2
def receive_message(self, srv, raw_msg, private=False, data=None): def receive_message(self, srv, raw_msg, private=False, data=None):
"""Queued the message for treatment""" """Queued the message for treatment"""
#print (raw_msg) #print (raw_msg)
self.cnsr_queue.put_nowait(consumer.MessageConsumer(srv, raw_msg, datetime.now(), private, data)) self.cnsr_queue.put_nowait(MessageConsumer(srv, raw_msg, datetime.now(), private, data))
# Launch a new thread if necessary # Launch a new thread if necessary
self.update_consumers() self._launch_consumers()
def add_networkbot(self, srv, dest, dcc=None): def add_networkbot(self, srv, dest, dcc=None):
@ -298,7 +362,7 @@ class Bot:
for srv in k: for srv in k:
self.servers[srv].disconnect() self.servers[srv].disconnect()
# Hooks cache # Hooks cache
def create_cache(self, name): def create_cache(self, name):
if name not in self.hooks_cache: if name not in self.hooks_cache:
@ -349,7 +413,7 @@ class Bot:
return self.hooks_cache[name] return self.hooks_cache[name]
# Treatment # Treatment
def check_rest_times(self, store, hook): def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time""" """Remove from store the hook if it has been executed given time"""
@ -416,6 +480,8 @@ class Bot:
return self.treat_ask(msg, srv) return self.treat_ask(msg, srv)
def treat_prvmsg(self, msg, srv): def treat_prvmsg(self, msg, srv):
msg.is_owner = msg.nick == srv.owner
# First, treat CTCP # First, treat CTCP
if msg.ctcp: if msg.ctcp:
if msg.cmds[0] in self.ctcp_capabilities: if msg.cmds[0] in self.ctcp_capabilities:
@ -625,7 +691,15 @@ def _help_msg(sndr, modules, cmd):
return res return res
def hotswap(bak): def hotswap(bak):
return Bot(bak.servers, bak.modules, bak.modules_path) new = Bot(str(bak.ip), bak.modules_paths, bak.data_path)
new.ctcp_capabilities = bak.ctcp_capabilities
new.servers = bak.servers
new.modules = bak.modules
new.modules_configuration = bak.modules_configuration
new.events = bak.events
new.hooks = bak.hooks
new.network = bak.network
return new
def reload(): def reload():
import imp import imp

View File

@ -16,6 +16,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from distutils.version import LooseVersion
from importlib.abc import Finder from importlib.abc import Finder
from importlib.abc import SourceLoader from importlib.abc import SourceLoader
import imp import imp
@ -23,6 +24,7 @@ import logging
import os import os
import sys import sys
from bot import __version__
import event import event
import exception import exception
import hooks import hooks
@ -40,7 +42,7 @@ class ModuleFinder(Finder):
#print ("looking for", fullname, "in", path) #print ("looking for", fullname, "in", path)
# Search only for new nemubot modules (packages init) # Search only for new nemubot modules (packages init)
if path is None: if path is None:
for mpath in self.context.modules_path: for mpath in self.context.modules_paths:
#print ("looking for", fullname, "in", mpath) #print ("looking for", fullname, "in", mpath)
if (os.path.isfile(mpath + fullname + ".py") or if (os.path.isfile(mpath + fullname + ".py") or
os.path.isfile(mpath + fullname + "/__init__.py")): os.path.isfile(mpath + fullname + "/__init__.py")):
@ -134,11 +136,11 @@ class ModuleLoader(SourceLoader):
if not hasattr(module, "nemubotversion"): if not hasattr(module, "nemubotversion"):
raise ImportError("Module `%s' is not a nemubot module."%self.name) raise ImportError("Module `%s' is not a nemubot module."%self.name)
# Check module version # Check module version
if module.nemubotversion != self.context.version: if LooseVersion(__version__) < LooseVersion(str(module.nemubotversion)):
raise ImportError("Module `%s' is not compatible with this " raise ImportError("Module `%s' is not compatible with this "
"version." % self.name) "version." % self.name)
# Set module common functions and datas # Set module common functions and data
module.__LOADED__ = True module.__LOADED__ = True
module.logger = logging.getLogger("nemubot.module." + fullname) module.logger = logging.getLogger("nemubot.module." + fullname)
@ -151,7 +153,7 @@ class ModuleLoader(SourceLoader):
module.logger.debug(*args) module.logger.debug(*args)
def mod_save(): def mod_save():
fpath = self.context.datas_path + "/" + module.name + ".xml" fpath = self.context.data_path + "/" + module.name + ".xml"
module.print_debug("Saving DATAS to " + fpath) module.print_debug("Saving DATAS to " + fpath)
module.DATAS.save(fpath) module.DATAS.save(fpath)
@ -189,7 +191,7 @@ class ModuleLoader(SourceLoader):
module.del_event = del_event module.del_event = del_event
if not hasattr(module, "NODATA"): if not hasattr(module, "NODATA"):
module.DATAS = xmlparser.parse_file(self.context.datas_path module.DATAS = xmlparser.parse_file(self.context.data_path
+ module.name + ".xml") + module.name + ".xml")
module.save = mod_save module.save = mod_save
else: else:

View File

@ -45,7 +45,7 @@ if __name__ == "__main__":
logger.addHandler(fh) logger.addHandler(fh)
# Create bot context # Create bot context
context = bot.Bot(0, "FIXME") context = bot.Bot()
# Load the prompt # Load the prompt
prmpt = prompt.Prompt() prmpt = prompt.Prompt()
@ -66,7 +66,7 @@ if __name__ == "__main__":
else: else:
load_file(arg, context) load_file(arg, context)
print ("Nemubot v%s ready, my PID is %i!" % (context.version_txt, print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__,
os.getpid())) os.getpid()))
while prmpt.run(context): while prmpt.run(context):
try: try:
@ -79,7 +79,7 @@ if __name__ == "__main__":
# Reload all other modules # Reload all other modules
bot.reload() bot.reload()
print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
context.version_txt) bot.__version__)
except: except:
logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0"
"m Fix them before trying to reload the prompt.") "m Fix them before trying to reload the prompt.")