Backport part of v4 Bot class
This commit is contained in:
parent
a8fe4c5159
commit
8d1919a36b
222
bot.py
222
bot.py
@ -20,12 +20,16 @@ from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from queue import Queue
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
import uuid
|
||||
|
||||
import consumer
|
||||
import event
|
||||
__version__ = '3.4.dev0'
|
||||
__author__ = 'nemunaire'
|
||||
|
||||
from consumer import Consumer, EventConsumer, MessageConsumer
|
||||
from event import ModuleEvent
|
||||
import hooks
|
||||
from networkbot import NetworkBot
|
||||
from server.IRC import IRCServer
|
||||
@ -34,18 +38,29 @@ import response
|
||||
|
||||
logger = logging.getLogger("nemubot.bot")
|
||||
|
||||
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
class Bot:
|
||||
def __init__(self, ip, realname, mp=list()):
|
||||
# Bot general informations
|
||||
self.version = 3.4
|
||||
self.version_txt = "3.4-dev"
|
||||
logger.info("Initiate nemubot v%s", self.version_txt)
|
||||
|
||||
"""Class containing the bot context and ensuring key goals"""
|
||||
|
||||
def __init__(self, ip="127.0.0.1", modules_paths=list(), data_path="./datas/"):
|
||||
"""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
|
||||
self.ip = ip
|
||||
self.realname = realname
|
||||
self.ctcp_capabilities = dict()
|
||||
self.init_ctcp_capabilities()
|
||||
|
||||
@ -54,10 +69,6 @@ class Bot:
|
||||
self.modules = dict()
|
||||
self.modules_configuration = dict()
|
||||
|
||||
# Context paths
|
||||
self.modules_path = mp
|
||||
self.datas_path = './datas/'
|
||||
|
||||
# Events
|
||||
self.events = list()
|
||||
self.event_timer = None
|
||||
@ -85,13 +96,13 @@ class Bot:
|
||||
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
|
||||
self.ctcp_capabilities["DCC"] = self._ctcp_dcc
|
||||
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(
|
||||
msg.sender, "TIME %s" % (datetime.now()))
|
||||
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(
|
||||
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))
|
||||
|
||||
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])
|
||||
|
||||
|
||||
# Events methods
|
||||
|
||||
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:
|
||||
# Find an ID
|
||||
now = datetime.now()
|
||||
evt.id = "%d%c%d%d%c%d%d%c%d" % (now.year, ID_letters[now.microsecond % 52],
|
||||
now.month, now.day, ID_letters[now.microsecond % 42],
|
||||
now.hour, now.minute, ID_letters[now.microsecond % 32],
|
||||
now.second)
|
||||
eid = uuid.uuid1()
|
||||
|
||||
# Fill the id field of the event
|
||||
if type(eid) is uuid.UUID:
|
||||
evt.id = str(eid)
|
||||
else:
|
||||
# Ok, this is quite useless...
|
||||
try:
|
||||
evt.id = str(uuid.UUID(eid))
|
||||
except ValueError:
|
||||
evt.id = eid
|
||||
|
||||
# Add the event in place
|
||||
# Add the event in its place
|
||||
t = evt.current
|
||||
i = -1
|
||||
i = 0 # sentinel
|
||||
for i in range(0, len(self.events)):
|
||||
if self.events[i].current > t:
|
||||
i -= 1
|
||||
break
|
||||
self.events.insert(i + 1, evt)
|
||||
if i == -1:
|
||||
self.update_timer()
|
||||
if len(self.events) <= 0 or self.events[i+1] != evt:
|
||||
self.events.insert(i, evt)
|
||||
|
||||
if i == 0:
|
||||
# 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
|
||||
|
||||
# Register the event in the source module
|
||||
if module_src is not None:
|
||||
module_src.REGISTERED_EVENTS.append(evt.id)
|
||||
evt.module_src = module_src
|
||||
|
||||
logger.info("New event registered: %s -> %s", evt.id, evt)
|
||||
return evt.id
|
||||
|
||||
def del_event(self, id, module_src=None):
|
||||
"""Find and remove an event from list"""
|
||||
logger.info("Removing event: %s from %s", id, module_src)
|
||||
|
||||
def del_event(self, evt, module_src=None):
|
||||
"""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:
|
||||
self.events.remove(self.events[0])
|
||||
self.update_timer()
|
||||
self._update_event_timer()
|
||||
if module_src is not None:
|
||||
module_src.REGISTERED_EVENTS.remove(evt.id)
|
||||
return True
|
||||
@ -160,35 +213,52 @@ class Bot:
|
||||
return True
|
||||
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
|
||||
if self.event_timer is not None:
|
||||
self.event_timer.cancel()
|
||||
|
||||
if len(self.events) > 0:
|
||||
logger.debug("Update timer: next event in %d seconds",
|
||||
self.events[0].time_left.seconds)
|
||||
if datetime.now() + timedelta(seconds=5) >= self.events[0].current:
|
||||
while datetime.now() < self.events[0].current:
|
||||
time.sleep(0.6)
|
||||
self.end_timer()
|
||||
self._end_event_timer()
|
||||
else:
|
||||
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()
|
||||
else:
|
||||
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):
|
||||
@ -207,6 +277,21 @@ class Bot:
|
||||
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):
|
||||
"""Add a module to the context, if already exists, unload the
|
||||
old one before"""
|
||||
@ -220,19 +305,6 @@ class Bot:
|
||||
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):
|
||||
"""Unload a module"""
|
||||
if name in self.modules:
|
||||
@ -252,22 +324,14 @@ class Bot:
|
||||
return True
|
||||
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):
|
||||
"""Queued the message for treatment"""
|
||||
#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
|
||||
self.update_consumers()
|
||||
self._launch_consumers()
|
||||
|
||||
|
||||
def add_networkbot(self, srv, dest, dcc=None):
|
||||
@ -416,6 +480,8 @@ class Bot:
|
||||
return self.treat_ask(msg, srv)
|
||||
|
||||
def treat_prvmsg(self, msg, srv):
|
||||
msg.is_owner = msg.nick == srv.owner
|
||||
|
||||
# First, treat CTCP
|
||||
if msg.ctcp:
|
||||
if msg.cmds[0] in self.ctcp_capabilities:
|
||||
@ -625,7 +691,15 @@ def _help_msg(sndr, modules, cmd):
|
||||
return res
|
||||
|
||||
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():
|
||||
import imp
|
||||
|
12
importer.py
12
importer.py
@ -16,6 +16,7 @@
|
||||
# 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/>.
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from importlib.abc import Finder
|
||||
from importlib.abc import SourceLoader
|
||||
import imp
|
||||
@ -23,6 +24,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from bot import __version__
|
||||
import event
|
||||
import exception
|
||||
import hooks
|
||||
@ -40,7 +42,7 @@ class ModuleFinder(Finder):
|
||||
#print ("looking for", fullname, "in", path)
|
||||
# Search only for new nemubot modules (packages init)
|
||||
if path is None:
|
||||
for mpath in self.context.modules_path:
|
||||
for mpath in self.context.modules_paths:
|
||||
#print ("looking for", fullname, "in", mpath)
|
||||
if (os.path.isfile(mpath + fullname + ".py") or
|
||||
os.path.isfile(mpath + fullname + "/__init__.py")):
|
||||
@ -134,11 +136,11 @@ class ModuleLoader(SourceLoader):
|
||||
if not hasattr(module, "nemubotversion"):
|
||||
raise ImportError("Module `%s' is not a nemubot module."%self.name)
|
||||
# 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 "
|
||||
"version." % self.name)
|
||||
|
||||
# Set module common functions and datas
|
||||
# Set module common functions and data
|
||||
module.__LOADED__ = True
|
||||
module.logger = logging.getLogger("nemubot.module." + fullname)
|
||||
|
||||
@ -151,7 +153,7 @@ class ModuleLoader(SourceLoader):
|
||||
module.logger.debug(*args)
|
||||
|
||||
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.DATAS.save(fpath)
|
||||
|
||||
@ -189,7 +191,7 @@ class ModuleLoader(SourceLoader):
|
||||
module.del_event = del_event
|
||||
|
||||
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.save = mod_save
|
||||
else:
|
||||
|
@ -45,7 +45,7 @@ if __name__ == "__main__":
|
||||
logger.addHandler(fh)
|
||||
|
||||
# Create bot context
|
||||
context = bot.Bot(0, "FIXME")
|
||||
context = bot.Bot()
|
||||
|
||||
# Load the prompt
|
||||
prmpt = prompt.Prompt()
|
||||
@ -66,7 +66,7 @@ if __name__ == "__main__":
|
||||
else:
|
||||
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()))
|
||||
while prmpt.run(context):
|
||||
try:
|
||||
@ -79,7 +79,7 @@ if __name__ == "__main__":
|
||||
# Reload all other modules
|
||||
bot.reload()
|
||||
print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
|
||||
context.version_txt)
|
||||
bot.__version__)
|
||||
except:
|
||||
logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0"
|
||||
"m Fix them before trying to reload the prompt.")
|
||||
|
Loading…
Reference in New Issue
Block a user