Compare commits

...

12 commits

13 changed files with 265 additions and 612 deletions

View file

@ -1,204 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
import traceback
import sys
from nemubot.hooks import hook
nemubotversion = 3.4
NODATA = True
def getserver(toks, context, prompt, mandatory=False, **kwargs):
"""Choose the server in toks or prompt.
This function modify the tokens list passed as argument"""
if len(toks) > 1 and toks[1] in context.servers:
return context.servers[toks.pop(1)]
elif not mandatory or prompt.selectedServer:
return prompt.selectedServer
else:
from nemubot.prompt.error import PromptError
raise PromptError("Please SELECT a server or give its name in argument.")
@hook("prompt_cmd", "close")
def close(toks, context, **kwargs):
"""Disconnect and forget (remove from the servers list) the server"""
srv = getserver(toks, context=context, mandatory=True, **kwargs)
if srv.close():
del context.servers[srv.id]
return 0
return 1
@hook("prompt_cmd", "connect")
def connect(toks, **kwargs):
"""Make the connexion to a server"""
srv = getserver(toks, mandatory=True, **kwargs)
return not srv.open()
@hook("prompt_cmd", "disconnect")
def disconnect(toks, **kwargs):
"""Close the connection to a server"""
srv = getserver(toks, mandatory=True, **kwargs)
return not srv.close()
@hook("prompt_cmd", "discover")
def discover(toks, context, **kwargs):
"""Discover a new bot on a server"""
srv = getserver(toks, context=context, mandatory=True, **kwargs)
if len(toks) > 1 and "!" in toks[1]:
bot = context.add_networkbot(srv, name)
return not bot.connect()
else:
print(" %s is not a valid fullname, for example: "
"nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1]))
return 1
@hook("prompt_cmd", "join")
@hook("prompt_cmd", "leave")
@hook("prompt_cmd", "part")
def join(toks, **kwargs):
"""Join or leave a channel"""
srv = getserver(toks, mandatory=True, **kwargs)
if len(toks) <= 2:
print("%s: not enough arguments." % toks[0])
return 1
if toks[0] == "join":
if len(toks) > 2:
srv.write("JOIN %s %s" % (toks[1], toks[2]))
else:
srv.write("JOIN %s" % toks[1])
elif toks[0] == "leave" or toks[0] == "part":
if len(toks) > 2:
srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:])))
else:
srv.write("PART %s" % toks[1])
return 0
@hook("prompt_cmd", "save")
def save_mod(toks, context, **kwargs):
"""Force save module data"""
if len(toks) < 2:
print("save: not enough arguments.")
return 1
wrn = 0
for mod in toks[1:]:
if mod in context.modules:
context.modules[mod].save()
print("save: module `%s´ saved successfully" % mod)
else:
wrn += 1
print("save: no module named `%s´" % mod)
return wrn
@hook("prompt_cmd", "send")
def send(toks, **kwargs):
"""Send a message on a channel"""
srv = getserver(toks, mandatory=True, **kwargs)
# Check the server is connected
if not srv.connected:
print ("send: server `%s' not connected." % srv.id)
return 2
if len(toks) <= 3:
print ("send: not enough arguments.")
return 1
if toks[1] not in srv.channels:
print ("send: channel `%s' not authorized in server `%s'."
% (toks[1], srv.id))
return 3
from nemubot.message import Text
srv.send_response(Text(" ".join(toks[2:]), server=None,
to=[toks[1]]))
return 0
@hook("prompt_cmd", "zap")
def zap(toks, **kwargs):
"""Hard change connexion state"""
srv = getserver(toks, mandatory=True, **kwargs)
srv.connected = not srv.connected
@hook("prompt_cmd", "top")
def top(toks, context, **kwargs):
"""Display consumers load information"""
print("Queue size: %d, %d thread(s) running (counter: %d)" %
(context.cnsr_queue.qsize(),
len(context.cnsr_thrd),
context.cnsr_thrd_size))
if len(context.events) > 0:
print("Events registered: %d, next in %d seconds" %
(len(context.events),
context.events[0].time_left.seconds))
else:
print("No events registered")
for th in context.cnsr_thrd:
if th.is_alive():
print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) %
th.ident)
traceback.print_stack(sys._current_frames()[th.ident])
@hook("prompt_cmd", "netstat")
def netstat(toks, context, **kwargs):
"""Display sockets in use and many other things"""
if len(context.network) > 0:
print("Distant bots connected: %d:" % len(context.network))
for name, bot in context.network.items():
print("# %s:" % name)
print(" * Declared hooks:")
lvl = 0
for hlvl in bot.hooks:
lvl += 1
for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp +
hlvl.cmd_default + hlvl.ask_rgxp +
hlvl.ask_default + hlvl.msg_rgxp +
hlvl.msg_default):
print(" %s- %s" % (' ' * lvl * 2, hook))
for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]:
print(" %s- <%s> %s" % (' ' * lvl * 2, kind,
", ".join(hlvl.__dict__[kind].keys())))
print(" * My tag: %d" % bot.my_tag)
print(" * Tags in use (%d):" % bot.inc_tag)
for tag, (cmd, data) in bot.tags.items():
print(" - %11s: %s « %s »" % (tag, cmd, data))
else:
print("No distant bot connected")

View file

@ -20,6 +20,7 @@ __version__ = '4.0.dev3'
__author__ = 'nemunaire' __author__ = 'nemunaire'
from nemubot.modulecontext import ModuleContext from nemubot.modulecontext import ModuleContext
context = ModuleContext(None, None) context = ModuleContext(None, None)
@ -40,12 +41,112 @@ def requires_version(min=None, max=None):
"but this is nemubot v%s." % (str(max), __version__)) "but this is nemubot v%s." % (str(max), __version__))
def attach(pid, socketfile):
import socket
import sys
print("nemubot is already launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile))
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(socketfile)
except socket.error as e:
sys.stderr.write(str(e))
sys.stderr.write("\n")
return 1
from select import select
try:
print("Connection established.")
while True:
rl, wl, xl = select([sys.stdin, sock], [], [])
if sys.stdin in rl:
line = sys.stdin.readline().strip()
if line == "exit" or line == "quit":
return 0
elif line == "reload":
import os, signal
os.kill(pid, signal.SIGHUP)
print("Reload signal sent. Please wait...")
elif line == "shutdown":
import os, signal
os.kill(pid, signal.SIGTERM)
print("Shutdown signal sent. Please wait...")
elif line == "kill":
import os, signal
os.kill(pid, signal.SIGKILL)
print("Signal sent...")
return 0
elif line == "stack" or line == "stacks":
import os, signal
os.kill(pid, signal.SIGUSR1)
print("Debug signal sent. Consult logs.")
else:
sock.send(line.encode() + b'\r\n')
if sock in rl:
sys.stdout.write(sock.recv(2048).decode())
except KeyboardInterrupt:
pass
except:
return 1
finally:
sock.close()
return 0
def daemonize():
"""Detach the running process to run as a daemon
"""
import os
import sys
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as err:
sys.stderr.write("Unable to fork: %s\n" % err)
sys.exit(1)
os.setsid()
os.umask(0)
os.chdir('/')
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as err:
sys.stderr.write("Unable to fork: %s\n" % err)
sys.exit(1)
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
def reload(): def reload():
"""Reload code of all Python modules used by nemubot """Reload code of all Python modules used by nemubot
""" """
import imp import imp
import nemubot.bot
imp.reload(nemubot.bot)
import nemubot.channel import nemubot.channel
imp.reload(nemubot.channel) imp.reload(nemubot.channel)
@ -71,15 +172,14 @@ def reload():
nemubot.message.reload() nemubot.message.reload()
import nemubot.prompt
imp.reload(nemubot.prompt)
nemubot.prompt.reload()
import nemubot.server import nemubot.server
rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist rl = nemubot.server._rlist
wl = nemubot.server._wlist
xl = nemubot.server._xlist
imp.reload(nemubot.server) imp.reload(nemubot.server)
nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl nemubot.server._rlist = rl
nemubot.server._wlist = wl
nemubot.server._xlist = xl
nemubot.server.reload() nemubot.server.reload()

View file

@ -18,6 +18,7 @@
def main(): def main():
import os import os
import signal
import sys import sys
# Parse command line arguments # Parse command line arguments
@ -38,6 +39,15 @@ def main():
default=["./modules/"], default=["./modules/"],
help="directory to use as modules store") help="directory to use as modules store")
parser.add_argument("-d", "--debug", action="store_true",
help="don't deamonize, keep in foreground")
parser.add_argument("-P", "--pidfile", default="./nemubot.pid",
help="Path to the file where store PID")
parser.add_argument("-S", "--socketfile", default="./nemubot.sock",
help="path where open the socket for internal communication")
parser.add_argument("-l", "--logfile", default="./nemubot.log", parser.add_argument("-l", "--logfile", default="./nemubot.log",
help="Path to store logs") help="Path to store logs")
@ -60,10 +70,34 @@ def main():
# Resolve relatives paths # Resolve relatives paths
args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) args.data_path = os.path.abspath(os.path.expanduser(args.data_path))
args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile))
args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile))
args.logfile = os.path.abspath(os.path.expanduser(args.logfile)) args.logfile = os.path.abspath(os.path.expanduser(args.logfile))
args.files = [ x for x in map(os.path.abspath, args.files)] args.files = [ x for x in map(os.path.abspath, args.files)]
args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)] args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)]
# Check if an instance is already launched
if args.pidfile is not None and os.path.isfile(args.pidfile):
with open(args.pidfile, "r") as f:
pid = int(f.readline())
try:
os.kill(pid, 0)
except OSError:
pass
else:
from nemubot import attach
sys.exit(attach(pid, args.socketfile))
# Daemonize
if not args.debug:
from nemubot import daemonize
daemonize()
# Store PID to pidfile
if args.pidfile is not None:
with open(args.pidfile, "w+") as f:
f.write(str(os.getpid()))
# Setup loggin interface # Setup loggin interface
import logging import logging
logger = logging.getLogger("nemubot") logger = logging.getLogger("nemubot")
@ -72,11 +106,12 @@ def main():
formatter = logging.Formatter( formatter = logging.Formatter(
'%(asctime)s %(name)s %(levelname)s %(message)s') '%(asctime)s %(name)s %(levelname)s %(message)s')
ch = logging.StreamHandler() if args.debug:
ch.setFormatter(formatter) ch = logging.StreamHandler()
if args.verbose < 2: ch.setFormatter(formatter)
ch.setLevel(logging.INFO) if args.verbose < 2:
logger.addHandler(ch) ch.setLevel(logging.INFO)
logger.addHandler(ch)
fh = logging.FileHandler(args.logfile) fh = logging.FileHandler(args.logfile)
fh.setFormatter(formatter) fh.setFormatter(formatter)
@ -100,13 +135,10 @@ def main():
if args.no_connect: if args.no_connect:
context.noautoconnect = True context.noautoconnect = True
# Load the prompt
import nemubot.prompt
prmpt = nemubot.prompt.Prompt()
# Register the hook for futur import # Register the hook for futur import
from nemubot.importer import ModuleFinder from nemubot.importer import ModuleFinder
sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) module_finder = ModuleFinder(context.modules_paths, context.add_module)
sys.meta_path.append(module_finder)
# Load requested configuration files # Load requested configuration files
for path in args.files: for path in args.files:
@ -119,36 +151,62 @@ def main():
for module in args.module: for module in args.module:
__import__(module) __import__(module)
print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, # Signals handling
os.getpid())) def sigtermhandler(signum, frame):
while True: """On SIGTERM and SIGINT, quit nicely"""
from nemubot.prompt.reset import PromptReset context.quit()
try: signal.signal(signal.SIGINT, sigtermhandler)
context.start() signal.signal(signal.SIGTERM, sigtermhandler)
if prmpt.run(context):
break
except PromptReset as e:
if e.type == "quit":
break
try: def sighuphandler(signum, frame):
import imp """On SIGHUP, perform a deep reload"""
# Reload all other modules import imp
imp.reload(nemubot) nonlocal nemubot, context, module_finder
imp.reload(nemubot.prompt)
nemubot.reload()
import nemubot.bot
context = nemubot.bot.hotswap(context)
prmpt = nemubot.prompt.hotswap(prmpt)
print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
nemubot.__version__)
except:
logger.exception("\033[1;31mUnable to reload the prompt due to "
"errors.\033[0m Fix them before trying to reload "
"the prompt.")
context.quit() logger.debug("SIGHUP receive, iniate reload procedure...")
print("Waiting for other threads shuts down...")
# Reload nemubot Python modules
imp.reload(nemubot)
nemubot.reload()
# Hotswap context
import nemubot.bot
context = nemubot.bot.hotswap(context)
# Reload ModuleFinder
sys.meta_path.remove(module_finder)
module_finder = ModuleFinder(context.modules_paths, context.add_module)
sys.meta_path.append(module_finder)
# Reload configuration file
for path in args.files:
if os.path.isfile(path):
context.sync_queue.put_nowait(["loadconf", path])
signal.signal(signal.SIGHUP, sighuphandler)
def sigusr1handler(signum, frame):
"""On SIGHUSR1, display stacktraces"""
import traceback
for threadId, stack in sys._current_frames().items():
logger.debug("########### Thread %d:\n%s",
threadId,
"".join(traceback.format_stack(stack)))
signal.signal(signal.SIGUSR1, sigusr1handler)
if args.socketfile:
from nemubot.server.socket import SocketListener
context.add_server(SocketListener(context.add_server, "master_socket",
sock_location=args.socketfile))
# context can change when performing an hotswap, always join the latest context
oldcontext = None
while oldcontext != context:
oldcontext = context
context.start()
context.join()
# Wait for consumers
logger.info("Waiting for other threads shuts down...")
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -143,6 +143,7 @@ class Bot(threading.Thread):
from select import select from select import select
from nemubot.server import _lock, _rlist, _wlist, _xlist from nemubot.server import _lock, _rlist, _wlist, _xlist
logger.info("Starting main loop")
self.stop = False self.stop = False
while not self.stop: while not self.stop:
with _lock: with _lock:
@ -208,6 +209,7 @@ class Bot(threading.Thread):
from nemubot.tools.config import load_file from nemubot.tools.config import load_file
load_file(path, self) load_file(path, self)
self.sync_queue.task_done() self.sync_queue.task_done()
logger.info("Ending main loop")
@ -509,14 +511,16 @@ def hotswap(bak):
bak.stop = True bak.stop = True
if bak.event_timer is not None: if bak.event_timer is not None:
bak.event_timer.cancel() bak.event_timer.cancel()
# Unload modules
for mod in [k for k in bak.modules.keys()]:
bak.unload_module(mod)
# Save datastore
bak.datastore.close() bak.datastore.close()
new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) new = Bot(str(bak.ip), bak.modules_paths, bak.datastore)
new.servers = bak.servers new.servers = bak.servers
new.modules = bak.modules
new.modules_configuration = bak.modules_configuration
new.events = bak.events
new.hooks = bak.hooks
new._update_event_timer() new._update_event_timer()
return new return new

View file

@ -24,8 +24,8 @@ class ModuleEvent:
"""Representation of a event initiated by a bot module""" """Representation of a event initiated by a bot module"""
def __init__(self, call=None, call_data=None, func=None, func_data=None, def __init__(self, call=None, call_data=None, func=None, func_data=None,
cmp=None, cmp_data=None, interval=60, offset=0, times=1): cmp=None, cmp_data=None, end_call=None, end_data=None,
interval=60, offset=0, times=1, max_attempt=-1):
"""Initialize the event """Initialize the event
Keyword arguments: Keyword arguments:
@ -35,9 +35,12 @@ class ModuleEvent:
func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch
cmp -- Boolean function called to check changes cmp -- Boolean function called to check changes
cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous
end_call -- Function called when times or max_attempt reach 0 (mainly for interaction with the event manager)
end_data -- Argument(s) (single or dict) to pass as argument
interval -- Time in seconds between each check (default: 60) interval -- Time in seconds between each check (default: 60)
offset -- Time in seconds added to interval before the first check (default: 0) offset -- Time in seconds added to interval before the first check (default: 0)
times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1) times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1)
max_attempt -- Maximum number of times the event will be checked
""" """
# What have we to check? # What have we to check?
@ -46,16 +49,17 @@ class ModuleEvent:
# How detect a change? # How detect a change?
self.cmp = cmp self.cmp = cmp
self.cmp_data = None
if cmp_data is not None: if cmp_data is not None:
self.cmp_data = cmp_data self.cmp_data = cmp_data
elif self.func is not None: elif callable(self.func):
if self.func_data is None: if self.func_data is None:
self.cmp_data = self.func() self.cmp_data = self.func()
elif isinstance(self.func_data, dict): elif isinstance(self.func_data, dict):
self.cmp_data = self.func(**self.func_data) self.cmp_data = self.func(**self.func_data)
else: else:
self.cmp_data = self.func(self.func_data) self.cmp_data = self.func(self.func_data)
else:
self.cmp_data = None
# What should we call when? # What should we call when?
self.call = call self.call = call
@ -64,46 +68,29 @@ class ModuleEvent:
else: else:
self.call_data = func_data self.call_data = func_data
# Store times # Store time between each event
self.offset = timedelta(seconds=offset) # Time to wait before the first check
self.interval = timedelta(seconds=interval) self.interval = timedelta(seconds=interval)
self._end = None # Cache
# How many times do this event? # How many times do this event?
self.times = times self.times = times
@property # Cache the time of the next occurence
def current(self): self.next_occur = datetime.now(timezone.utc) + timedelta(seconds=offset) + self.interval
"""Return the date of the near check"""
if self.times != 0:
if self._end is None:
self._end = datetime.now(timezone.utc) + self.offset + self.interval
return self._end
return None
@property
def next(self):
"""Return the date of the next check"""
if self.times != 0:
if self._end is None:
return self.current
elif self._end < datetime.now(timezone.utc):
self._end += self.interval
return self._end
return None
@property @property
def time_left(self): def time_left(self):
"""Return the time left before/after the near check""" """Return the time left before/after the near check"""
if self.current is not None:
return self.current - datetime.now(timezone.utc) return self.next_occur - datetime.now(timezone.utc)
return 99999 # TODO: 99999 is not a valid time to return
def check(self): def check(self):
"""Run a check and realized the event if this is time""" """Run a check and realized the event if this is time"""
self.max_attempt -= 1
# Get initial data # Get initial data
if self.func is None: if not callable(self.func):
d_init = self.func_data d_init = self.func_data
elif self.func_data is None: elif self.func_data is None:
d_init = self.func() d_init = self.func()
@ -113,7 +100,7 @@ class ModuleEvent:
d_init = self.func(self.func_data) d_init = self.func(self.func_data)
# then compare with current data # then compare with current data
if self.cmp is None: if not callable(self.cmp):
if self.cmp_data is None: if self.cmp_data is None:
rlz = True rlz = True
else: else:
@ -138,3 +125,19 @@ class ModuleEvent:
self.call(d_init, **self.call_data) self.call(d_init, **self.call_data)
else: else:
self.call(d_init, self.call_data) self.call(d_init, self.call_data)
# Is it finished?
if self.times == 0 or self.max_attempt == 0:
if not callable(self.end_call):
pass # TODO: log a WARN here
else:
if self.end_data is None:
self.end_call()
elif isinstance(self.end_data, dict):
self.end_call(**self.end_data)
else:
self.end_call(self.end_data)
# Not finished, ready to next one!
else:
self.next_occur += self.interval

View file

@ -51,7 +51,7 @@ class ModuleContext:
self.config = ModuleState("module") self.config = ModuleState("module")
self.hooks = list() self.hooks = list()
self.events = list() self.events = list() # Un eventManager, qui contient une liste globale et un thread global et quelques méthodes statique, mais chaque événement est exécuté dans son propre contexte :)
self.debug = context.verbosity > 0 if context is not None else False self.debug = context.verbosity > 0 if context is not None else False
# Define some callbacks # Define some callbacks
@ -79,7 +79,9 @@ class ModuleContext:
def subtreat(msg): def subtreat(msg):
yield from context.treater.treat_msg(msg) yield from context.treater.treat_msg(msg)
def add_event(evt, eid=None): def add_event(evt, eid=None):
return context.add_event(evt, eid, module_src=module) eid = context.add_event(evt, eid, module_src=module)
self.events.append(eid)
return eid
def del_event(evt): def del_event(evt):
return context.del_event(evt, module_src=module) return context.del_event(evt, module_src=module)

View file

@ -1,144 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
import shlex
import sys
import traceback
from nemubot.prompt import builtins
class Prompt:
def __init__(self):
self.selectedServer = None
self.lastretcode = 0
self.HOOKS_CAPS = dict()
self.HOOKS_LIST = dict()
def add_cap_hook(self, name, call, data=None):
self.HOOKS_CAPS[name] = lambda t, c: call(t, data=data,
context=c, prompt=self)
def add_list_hook(self, name, call):
self.HOOKS_LIST[name] = call
def lex_cmd(self, line):
"""Return an array of tokens
Argument:
line -- the line to lex
"""
try:
cmds = shlex.split(line)
except:
exc_type, exc_value, _ = sys.exc_info()
sys.stderr.write(traceback.format_exception_only(exc_type,
exc_value)[0])
return
bgn = 0
# Separate commands (command separator: ;)
for i in range(0, len(cmds)):
if cmds[i][-1] == ';':
if i != bgn:
yield cmds[bgn:i]
bgn = i + 1
# Return rest of the command (that not end with a ;)
if bgn != len(cmds):
yield cmds[bgn:]
def exec_cmd(self, toks, context):
"""Execute the command
Arguments:
toks -- lexed tokens to executes
context -- current bot context
"""
if toks[0] in builtins.CAPS:
self.lastretcode = builtins.CAPS[toks[0]](toks, context, self)
elif toks[0] in self.HOOKS_CAPS:
self.lastretcode = self.HOOKS_CAPS[toks[0]](toks, context)
else:
print("Unknown command: `%s'" % toks[0])
self.lastretcode = 127
def getPS1(self):
"""Get the PS1 associated to the selected server"""
if self.selectedServer is None:
return "nemubot"
else:
return self.selectedServer.id
def run(self, context):
"""Launch the prompt
Argument:
context -- current bot context
"""
from nemubot.prompt.error import PromptError
from nemubot.prompt.reset import PromptReset
while True: # Stopped by exception
try:
line = input("\033[0;33m%s\033[0;%d\033[0m " %
(self.getPS1(), 31 if self.lastretcode else 32))
cmds = self.lex_cmd(line.strip())
for toks in cmds:
try:
self.exec_cmd(toks, context)
except PromptReset:
raise
except PromptError as e:
print(e.message)
self.lastretcode = 128
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value,
exc_traceback)
except KeyboardInterrupt:
print("")
except EOFError:
print("quit")
return True
def hotswap(bak):
p = Prompt()
p.HOOKS_CAPS = bak.HOOKS_CAPS
p.HOOKS_LIST = bak.HOOKS_LIST
return p
def reload():
import imp
import nemubot.prompt.builtins
imp.reload(nemubot.prompt.builtins)
import nemubot.prompt.error
imp.reload(nemubot.prompt.error)
import nemubot.prompt.reset
imp.reload(nemubot.prompt.reset)

View file

@ -1,132 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
def end(toks, context, prompt):
"""Quit the prompt for reload or exit"""
from nemubot.prompt.reset import PromptReset
if toks[0] == "refresh":
raise PromptReset("refresh")
elif toks[0] == "reset":
raise PromptReset("reset")
raise PromptReset("quit")
def liste(toks, context, prompt):
"""Show some lists"""
if len(toks) > 1:
for l in toks[1:]:
l = l.lower()
if l == "server" or l == "servers":
for srv in context.servers.keys():
print (" - %s (state: %s) ;" % (srv,
"connected" if context.servers[srv].connected else "disconnected"))
if len(context.servers) == 0:
print (" > No server loaded")
elif l == "mod" or l == "mods" or l == "module" or l == "modules":
for mod in context.modules.keys():
print (" - %s ;" % mod)
if len(context.modules) == 0:
print (" > No module loaded")
elif l in prompt.HOOKS_LIST:
f, d = prompt.HOOKS_LIST[l]
f(d, context, prompt)
else:
print (" Unknown list `%s'" % l)
return 2
return 0
else:
print (" Please give a list to show: servers, ...")
return 1
def load(toks, context, prompt):
"""Load an XML configuration file"""
if len(toks) > 1:
from nemubot.tools.config import load_file
for filename in toks[1:]:
load_file(filename, context)
else:
print ("Not enough arguments. `load' takes a filename.")
return 1
def select(toks, context, prompt):
"""Select the current server"""
if (len(toks) == 2 and toks[1] != "None" and
toks[1] != "nemubot" and toks[1] != "none"):
if toks[1] in context.servers:
prompt.selectedServer = context.servers[toks[1]]
else:
print ("select: server `%s' not found." % toks[1])
return 1
else:
prompt.selectedServer = None
def unload(toks, context, prompt):
"""Unload a module"""
if len(toks) == 2 and toks[1] == "all":
for name in context.modules.keys():
context.unload_module(name)
elif len(toks) > 1:
for name in toks[1:]:
if context.unload_module(name):
print (" Module `%s' successfully unloaded." % name)
else:
print (" No module `%s' loaded, can't unload!" % name)
return 2
else:
print ("Not enough arguments. `unload' takes a module name.")
return 1
def debug(toks, context, prompt):
"""Enable/Disable debug mode on a module"""
if len(toks) > 1:
for name in toks[1:]:
if name in context.modules:
context.modules[name].DEBUG = not context.modules[name].DEBUG
if context.modules[name].DEBUG:
print (" Module `%s' now in DEBUG mode." % name)
else:
print (" Debug for module module `%s' disabled." % name)
else:
print (" No module `%s' loaded, can't debug!" % name)
return 2
else:
print ("Not enough arguments. `debug' takes a module name.")
return 1
# Register build-ins
CAPS = {
'quit': end, # Disconnect all server and quit
'exit': end, # Alias for quit
'reset': end, # Reload the prompt
'refresh': end, # Reload the prompt but save modules
'load': load, # Load a servers or module configuration file
'unload': unload, # Unload a module and remove it from the list
'select': select, # Select a server
'list': liste, # Show lists
'debug': debug, # Pass a module in debug mode
}

View file

@ -1,23 +0,0 @@
# coding=utf-8
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
class PromptError(Exception):
def __init__(self, message):
super(PromptError, self).__init__(message)
self.message = message

View file

@ -1,23 +0,0 @@
# coding=utf-8
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
class PromptReset(Exception):
def __init__(self, type):
super(PromptReset, self).__init__("Prompt reset asked")
self.type = type

View file

@ -115,7 +115,7 @@ class AbstractServer(io.IOBase):
""" """
self._sending_queue.put(self.format(message)) self._sending_queue.put(self.format(message))
self.logger.debug("Message '%s' appended to Queue", message) self.logger.debug("Message '%s' appended to write queue", message)
if self not in _wlist: if self not in _wlist:
_wlist.append(self) _wlist.append(self)

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/>.
import nemubot.message as message
from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.message.printer.socket import Socket as SocketPrinter
from nemubot.server.abstract import AbstractServer from nemubot.server.abstract import AbstractServer
@ -132,6 +133,18 @@ class SocketServer(AbstractServer):
yield line yield line
def parse(self, line):
import shlex
line = line.strip().decode()
try:
args = shlex.split(line)
except ValueError:
args = line.split(' ')
yield message.Command(cmd=args[0], args=args[1:], server=self)
class SocketListener(AbstractServer): class SocketListener(AbstractServer):
def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None):

View file

@ -66,7 +66,6 @@ setup(
'nemubot.hooks', 'nemubot.hooks',
'nemubot.message', 'nemubot.message',
'nemubot.message.printer', 'nemubot.message.printer',
'nemubot.prompt',
'nemubot.server', 'nemubot.server',
'nemubot.server.message', 'nemubot.server.message',
'nemubot.tools', 'nemubot.tools',