diff --git a/DCC.py b/DCC.py index 48bb0b0..703bc90 100644 --- a/DCC.py +++ b/DCC.py @@ -26,18 +26,14 @@ import time import traceback import message +import server #Store all used ports PORTS = list() -class DCC(threading.Thread): +class DCC(server.Server): def __init__(self, srv, dest, socket=None): - self.DCC = False # Is this a DCC connection self.error = False # An error has occur, closing the connection? - self.stop = False # Stop requered - self.stopping = threading.Event() # Event to listen for full disconnection - self.conn = socket # The socket - self.connected = self.conn is not None # Is connected? self.messages = list() # Message queued before connexion # Informations about the sender @@ -64,7 +60,7 @@ class DCC(threading.Thread): threading.Thread.__init__(self) def foundPort(self): - """Found a free port for the connexion""" + """Found a free port for the connection""" for p in range(65432, 65535): if p not in PORTS: PORTS.append(p) @@ -73,41 +69,18 @@ class DCC(threading.Thread): @property def id(self): + """Gives the server identifiant""" return self.srv.id + "/" + self.sender def setError(self, msg): self.error = True self.srv.send_msg_usr(self.sender, msg) - def disconnect(self): - """Close the connection if connected""" - if self.connected: - self.stop = True - self.conn.shutdown(socket.SHUT_RDWR) - self.stopping.wait() - return True - return False - - def kill(self): - """Stop the loop without closing the socket""" - if self.connected: - self.stop = True - self.connected = False - #self.stopping.wait()#Compare with server before delete me - return True - return False - - def launch(self, mods=None): - """Connect to the client if not already connected""" - if not self.connected: - self.stop = False - self.start() - def accept_user(self, host, port): """Accept a DCC connection""" - self.conn = socket.socket() + self.s = socket.socket() try: - self.conn.connect((host, port)) + self.s.connect((host, port)) print ('Accepted user from', host, port, "for", self.sender) self.connected = True self.stop = False @@ -143,13 +116,13 @@ class DCC(threading.Thread): s.listen(1) #Waiting for the client - (self.conn, addr) = s.accept() + (self.s, addr) = s.accept() print ('Connected by', addr) self.connected = True return True def send_dcc_raw(self, line): - self.conn.sendall(line + b'\n') + self.s.sendall(line + b'\n') def send_dcc(self, msg, to = None): """If we talk to this user, send a message through this connection @@ -157,7 +130,7 @@ class DCC(threading.Thread): if to is None or to == self.sender or to == self.nick: if self.error: self.srv.send_msg_final(self.nick, msg) - elif not self.connected or self.conn is None: + elif not self.connected or self.s is None: if not self.DCC: self.start() self.DCC = True @@ -190,8 +163,8 @@ class DCC(threading.Thread): with open(self.messages, 'rb') as f: d = f.read(268435456) #Packets size: 256Mo while d: - self.conn.sendall(d) - self.conn.recv(4) #The client send a confirmation after each packet + self.s.sendall(d) + self.s.recv(4) #The client send a confirmation after each packet d = f.read(268435456) #Packets size: 256Mo # Messages connection @@ -211,7 +184,7 @@ class DCC(threading.Thread): self.nicksize = len(self.srv.nick) self.Bnick = self.srv.nick.encode() while not self.stop: - raw = self.conn.recv(1024) #recieve server messages + raw = self.s.recv(1024) #recieve server messages if not raw: break readbuffer = readbuffer + raw @@ -222,7 +195,7 @@ class DCC(threading.Thread): self.treatement(line) if self.connected: - self.conn.close() + self.s.close() self.connected = False #Remove from DCC connections server list diff --git a/IRCServer.py b/IRCServer.py new file mode 100644 index 0000000..2a3cfae --- /dev/null +++ b/IRCServer.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 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 . + +import errno +import os +import socket +import threading +import traceback + +import channel +import DCC +import message +import server +import xmlparser + +class IRCServer(server.Server): + """Class to interact with an IRC server""" + + def __init__(self, node, nick, owner, realname): + """Initialize an IRC server + + Arguments: + node -- server node from XML configuration + nick -- nick used by the bot on this server + owner -- nick used by the bot owner on this server + realname -- string used as realname on this server + + """ + server.Server.__init__(self) + + self.node = node + + self.nick = nick + self.owner = owner + self.realname = realname + + # Listen private messages? + self.listen_nick = True + + self.dcc_clients = dict() + + self.channels = dict() + for chn in self.node.getNodes("channel"): + chan = channel.Channel(chn, self) + self.channels[chan.name] = chan + + + @property + def host(self): + """Return the server hostname""" + if self.node is not None and self.node.hasAttribute("server"): + return self.node["server"] + else: + return "localhost" + + @property + def port(self): + """Return the connection port used on this server""" + if self.node is not None and self.node.hasAttribute("port"): + return self.node.getInt("port") + else: + return "6667" + + @property + def password(self): + """Return the password used to connect to this server""" + if self.node is not None and self.node.hasAttribute("password"): + return self.node["password"] + else: + return None + + @property + def allow_all(self): + """If True, treat message from all channels, not only listed one""" + return (self.node is not None and self.node.hasAttribute("allowall") + and self.node["allowall"] == "true") + + @property + def autoconnect(self): + """Autoconnect the server when added""" + if self.node is not None and self.node.hasAttribute("autoconnect"): + value = self.node["autoconnect"].lower() + return value != "no" and value != "off" and value != "false" + else: + return False + + @property + def id(self): + """Gives the server identifiant""" + return self.host + ":" + str(self.port) + + def accepted_channel(self, chan, sender = None): + """Return True if the channel (or the user) is authorized""" + if self.allow_all: + return True + elif self.listen_nick: + return (chan in self.channels and (sender is None or sender in + self.channels[chan].people) + ) or chan == self.nick + else: + return chan in self.channels and (sender is None or sender + in self.channels[chan].people) + + def join(self, chan, password=None): + """Join a channel""" + if chan is not None and self.connected and chan not in self.channels: + chn = xmlparser.module_state.ModuleState("channel") + chn["name"] = chan + chn["password"] = password + self.node.addChild(chn) + self.channels[chan] = channel.Channel(chn, self) + if password is not None: + self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode ()) + else: + self.s.send(("JOIN %s\r\n" % chan).encode ()) + return True + else: + return False + + def leave(self, chan): + """Leave a channel""" + if chan is not None and self.connected and chan in self.channels: + if chan.instanceof(list): + for c in chan: + self.leave(c) + else: + self.s.send(("PART %s\r\n" % self.channels[chan].name).encode ()) + del self.channels[chan] + return True + else: + return False + +# Main loop + def run(self): + if not self.connected: + self.s = socket.socket() #Create the socket + try: + self.s.connect((self.host, self.port)) #Connect to server + except socket.error as e: + self.s = None + print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s" + % (self.host, self.port, os.strerror(e.errno))) + return + self.stopping.clear() + + if self.password != None: + self.s.send(b"PASS " + self.password.encode () + b"\r\n") + self.s.send(("NICK %s\r\n" % self.nick).encode ()) + self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host, + self.realname)).encode ()) + raw = self.s.recv(1024) + if not raw: + print ("Unable to connect to %s:%d" % (self.host, self.port)) + return + self.connected = True + print ("Connection to %s:%d completed" % (self.host, self.port)) + + readbuffer = b'' #Here we store all the messages from server + while not self.stop: + readbuffer = readbuffer + raw + temp = readbuffer.split(b'\n') + readbuffer = temp.pop() + + for line in temp: + self.treat_msg(line) + raw = self.s.recv(1024) #recieve server messages + + if self.connected: + self.s.close() + self.connected = False + print ("Server `%s' successfully stopped." % self.id) + self.stopping.set() + # Rearm Thread + threading.Thread.__init__(self) + + +# Overwritted methods + + def disconnect(self): + """Close the socket with the server and all DCC client connections""" + #Close all DCC connection + for clt in self.dcc_clients: + self.dcc_clients[clt].disconnect() + return server.Server.disconnect(self) + + + +# Abstract methods + + def send_pong(self, cnt): + """Send a PONG command to the server with argument cnt""" + self.s.send(("PONG %s\r\n" % cnt).encode ()) + + def msg_treated(self, origin): + """Do nothing; here for implement abstract class""" + pass + + def send_dcc(self, msg, to): + """Send a message through DCC connection""" + if msg is not None and to is not None: + realname = to.split("!")[1] + if realname not in self.dcc_clients.keys(): + d = dcc.DCC(self, to) + self.dcc_clients[realname] = d + self.dcc_clients[realname].send_dcc(msg) + + def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"): + """Send a message without checks or format""" + #TODO: add something for post message treatment here + if channel == self.nick: + print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg) + traceback.print_stack() + if line is not None and channel is not None: + if self.s is None: + print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line)) + traceback.print_stack() + elif len(line) < 442: + self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) + else: + print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) + traceback.print_stack() + self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ()) + + def send_msg_usr(self, user, msg): + """Send a message to a user instead of a channel""" + if user is not None and user[0] != "#": + realname = user.split("!")[1] + if realname in self.dcc_clients or user in self.dcc_clients: + self.send_dcc(msg, user) + else: + for line in msg.split("\n"): + if line != "": + self.send_msg_final(user.split('!')[0], msg) + + def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): + """Send a message to a channel""" + if self.accepted_channel(channel): + server.Server.send_msg(self, channel, msg, cmd, endl) + + def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): + """Send a message to a channel, only if the source user is on this channel too""" + if self.accepted_channel(channel, sender): + self.send_msg_final(channel, msg, cmd, endl) + + def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"): + """Send a message to all channels on this server""" + for channel in self.channels.keys(): + self.send_msg(channel, msg, cmd, endl) diff --git a/bot.py b/bot.py index f0cb476..878cde9 100644 --- a/bot.py +++ b/bot.py @@ -26,19 +26,26 @@ import consumer import event import hooks from networkbot import NetworkBot -from server import Server +from IRCServer import IRCServer +import response ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" class Bot: - def __init__(self, servers=dict(), modules=dict(), mp=list()): + def __init__(self, ip, realname, mp=list()): # Bot general informations - self.version = 3.2 - self.version_txt = "3.2-dev" + self.version = 3.3 + self.version_txt = "3.3-dev" + + # Save various informations + self.ip = ip + self.realname = realname + self.ctcp_capabilities = dict() + self.init_ctcp_capabilities() # Keep global context: servers and modules - self.servers = servers - self.modules = modules + self.servers = dict() + self.modules = dict() # Context paths self.modules_path = mp @@ -61,6 +68,36 @@ class Bot: self.cnsr_thrd_size = -1 + def init_ctcp_capabilities(self): + """Reset existing CTCP capabilities to default one""" + self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive") + 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) + 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) + self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( + msg.sender, "VERSION nemubot v%s" % self.version_txt) + + def _ctcp_clientinfo(self, srv, msg): + """Response to CLIENTINFO CTCP message""" + return _ctcp_response(msg.sndr, + " ".join(self.ctcp_capabilities.keys())) + + def _ctcp_dcc(self, srv, msg): + """Response to DCC CTCP message""" + ip = self.srv.toIP(int(msg.cmds[3])) + conn = DCC(srv, msg.sender) + if conn.accept_user(ip, int(msg.cmds[4])): + srv.dcc_clients[conn.sender] = conn + conn.send_dcc("Hello %s!" % conn.nick) + else: + print ("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4])) + + def add_event(self, evt, eid=None): """Register an event and return its identifiant for futur update""" if eid is None: @@ -126,7 +163,7 @@ class Bot: 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(self, evt)) + self.cnsr_queue.put_nowait(consumer.EventConsumer(evt)) self.update_consumers() self.update_timer() @@ -134,11 +171,11 @@ class Bot: def addServer(self, node, nick, owner, realname): """Add a new server to the context""" - srv = Server(node, nick, owner, realname) + srv = IRCServer(node, nick, owner, realname) if srv.id not in self.servers: self.servers[srv.id] = srv if srv.autoconnect: - srv.launch(self) + srv.launch(self.receive_message) return True else: return False @@ -296,6 +333,16 @@ class Bot: self.check_rest_times(store, h) + def treat_post(self, msg): + """Treat a message before send""" + for h, lvl, store in self.create_cache("all_post"): + c = h.run(msg) + self.check_rest_times(store, h) + if not c: + return False + return True + + def treat_cmd(self, msg): """Treat a command message""" treated = list() @@ -401,6 +448,8 @@ class Bot: return treated +def _ctcp_response(sndr, msg): + return response.Response(sndr, msg, ctcp=True) def hotswap(bak): return Bot(bak.servers, bak.modules, bak.modules_path) diff --git a/consumer.py b/consumer.py index a71a51a..0789d40 100644 --- a/consumer.py +++ b/consumer.py @@ -17,12 +17,15 @@ # along with this program. If not, see . import queue +import re import threading import traceback import sys +import bot from message import Message from response import Response +import server class MessageConsumer: """Store a message before treating""" @@ -33,11 +36,64 @@ class MessageConsumer: self.prvt = prvt self.data = data - def run(self): - # Create, parse and treat the message + def treat_in(self, context, msg): + """Treat the input message""" + if msg.cmd == "PING": + self.srv.send_pong(msg.content) + else: + # TODO: Manage credits here + context.treat_pre(msg) + + if msg.cmd == "PRIVMSG" and msg.ctcp: + if msg.cmds[0] in context.ctcp_capabilities: + return context.ctcp_capabilities[msg.cmds[0]](self.srv, msg) + else: + return bot._ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") + elif msg.cmd == "PRIVMSG" and self.srv.accepted_channel(msg.channel): + return self.treat_prvmsg(context, msg) + # TODO: continue here + pass + + def treat_prvmsg(self, context, msg): + # Treat all messages starting with 'nemubot:' as distinct commands + if msg.content.find("%s:"%self.srv.nick) == 0: + # Remove the bot name + msg.content = msg.content[len(self.srv.nick)+1:].strip() + + # Treat ping + if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", + msg.content, re.I) is not None: + return Response(msg.sender, message="pong", + channel=msg.channel, nick=msg.nick) + + # Ask hooks + else: + return context.treat_ask(self) + + + def treat_out(self, context, res): + """Treat the output message""" + # Define the destination server + if (res.server is not None and + res.server.instanceof(string) and res.server in context.servers): + res.server = context.servers[res.server] + if (res.server is not None and + not res.server.instanceof(server.Server)): + print ("\033[1;35mWarning:\033[0m the server defined in this " + "response doesn't exist: %s" % (res.server)) + res.server = None + if res.server is None: + res.server = self.srv + + # Sent the message only if treat_post authorize it + if context.treat_post(res): + res.server.send_response(res, self.data) + + def run(self, context): + """Create, parse and treat the message""" try: msg = Message(self.srv, self.raw, self.time, self.prvt) - res = msg.treat() + res = self.treat_in(context, msg) except: print ("\033[1;31mERROR:\033[0m occurred during the " "processing of the message: %s" % self.raw) @@ -51,26 +107,25 @@ class MessageConsumer: if isinstance(res, list): for r in res: if isinstance(r, Response): - self.srv.send_response(r, self.data) + self.treat_out(context, r) elif isinstance(r, list): for s in r: if isinstance(s, Response): - self.srv.send_response(s, self.data) + self.treat_out(context, s) elif isinstance(res, Response): - self.srv.send_response(res, self.data) - + self.treat_out(context, res) + # Inform that the message has been treated self.srv.msg_treated(self.data) - + class EventConsumer: """Store a event before treating""" - def __init__(self, context, evt, timeout=20): - self.context = context + def __init__(self, evt, timeout=20): self.evt = evt self.timeout = timeout - def run(self): + def run(self, context): try: self.evt.launch_check() except: @@ -79,9 +134,9 @@ class EventConsumer: traceback.print_exception(exc_type, exc_value, exc_traceback) if self.evt.next is not None: - self.context.add_event(self.evt, self.evt.id) - - + context.add_event(self.evt, self.evt.id) + + class Consumer(threading.Thread): """Dequeue and exec requested action""" @@ -94,7 +149,7 @@ class Consumer(threading.Thread): try: while not self.stop: stm = self.context.cnsr_queue.get(True, 20) - stm.run() + stm.run(self.context) except queue.Empty: pass diff --git a/hooks.py b/hooks.py index d6a5641..34e5332 100644 --- a/hooks.py +++ b/hooks.py @@ -24,7 +24,7 @@ class MessagesHook: # Store specials hook self.all_pre = list() # Treated before any parse - #self.all_post = list() # Treated before send message to user + self.all_post = list() # Treated before send message to user # Store direct hook self.cmd_hook = dict() diff --git a/importer.py b/importer.py index 810daa1..3cc10ba 100644 --- a/importer.py +++ b/importer.py @@ -136,7 +136,7 @@ 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 module.nemubotversion > self.context.version: raise ImportError("Module `%s' is not compatible with this " "version." % self.name) diff --git a/message.py b/message.py index 0839c42..d6be735 100644 --- a/message.py +++ b/message.py @@ -73,9 +73,17 @@ class Message: self.channel = self.pickWords(words[2:]).decode() if self.cmd == 'PRIVMSG': - #Check for CTCP request - self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01) - self.content = self.pickWords(words[3:]) + # Check for CTCP request + self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01) + self.content = self.pickWords(words[3:]) + # If CTCP, remove 0x01 + if self.ctcp: + self.content = self.content[1:len(self.content)-1] + # Split content by words + try: + self.cmds = shlex.split(self.content.decode()) + except ValueError: + self.cmds = self.content.decode().split(' ') elif self.cmd == '353' and len(words) > 3: for i in range(2, len(words)): if words[i][0] == 58: @@ -125,8 +133,9 @@ class Message: def is_owner(self): return self.nick == self.srv.owner - def authorize(self): + def authorize_DEPRECATED(self): """Is nemubot listening for the sender on this channel?""" + # TODO: deprecated if self.srv.isDCC(self.sender): return True elif self.realname not in CREDITS: @@ -139,13 +148,7 @@ class Message: def treat(self): """Parse and treat the message""" - if self.cmd == "PING": - self.srv.send_pong(self.content) - elif self.cmd == "PRIVMSG" and self.ctcp: - self.parsectcp() - elif self.cmd == "PRIVMSG" and self.authorize(): - return self.parsemsg() - elif self.channel in self.srv.channels: + if self.channel in self.srv.channels: if self.cmd == "353": self.srv.channels[self.channel].parse353(self) elif self.cmd == "332": @@ -166,53 +169,13 @@ class Message: self.srv.channels[chn].part(self.nick) return None - def parsectcp(self): - """Parse CTCP requests""" - if self.content == '\x01CLIENTINFO\x01': - self.srv.send_ctcp(self.sender, "CLIENTINFO TIME USERINFO VERSION CLIENTINFO") - elif self.content == '\x01TIME\x01': - self.srv.send_ctcp(self.sender, "TIME %s" % (datetime.now())) - elif self.content == '\x01USERINFO\x01': - self.srv.send_ctcp(self.sender, "USERINFO %s" % (self.srv.realname)) - elif self.content == '\x01VERSION\x01': - self.srv.send_ctcp(self.sender, "VERSION nemubot v%s" % self.srv.context.version_txt) - elif self.content[:9] == '\x01DCC CHAT': - words = self.content[1:len(self.content) - 1].split(' ') - ip = self.srv.toIP(int(words[3])) - conn = DCC(self.srv, self.sender) - if conn.accept_user(ip, int(words[4])): - self.srv.dcc_clients[conn.sender] = conn - conn.send_dcc("Hello %s!" % conn.nick) - else: - print ("DCC: unable to connect to %s:%s" % (ip, words[4])) - elif self.content == '\x01NEMUBOT\x01': - self.srv.send_ctcp(self.sender, "NEMUBOT %f" % self.srv.context.version) - elif self.content[:7] != '\x01ACTION': - print (self.content) - self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request") - def reparsemsg(self): self.parsemsg() def parsemsg (self): - self.srv.context.treat_pre(self) - #Treat all messages starting with 'nemubot:' as distinct commands - if self.content.find("%s:"%self.srv.nick) == 0: - #Remove the bot name - self.content = self.content[len(self.srv.nick)+1:].strip() - messagel = self.content.lower() - - # Treat ping - if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", - messagel) is not None: - return Response(self.sender, message="pong", channel=self.channel, nick=self.nick) - - # Ask hooks - else: - return self.srv.context.treat_ask(self) #Owner commands - elif self.content[0] == '`' and self.sender == self.srv.owner: + if self.content[0] == '`' and self.sender == self.srv.owner: self.cmd = self.content[1:].split(' ') if self.cmd[0] == "ban": if len(self.cmd) > 1: diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 354e128..b86cd9c 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -50,12 +50,12 @@ def connect(data, toks, context, prompt): if len(toks) > 1: for s in toks[1:]: if s in context.servers: - context.servers[s].launch(context) + context.servers[s].launch(context.receive_message) else: print ("connect: server `%s' not found." % s) elif prompt.selectedServer is not None: - prompt.selectedServer.launch(context) + prompt.selectedServer.launch(context.receive_message) else: print (" Please SELECT a server or give its name in argument.") diff --git a/nemubot.py b/nemubot.py index f984e25..5948304 100755 --- a/nemubot.py +++ b/nemubot.py @@ -29,7 +29,7 @@ import importer if __name__ == "__main__": # Create bot context - context = bot.Bot() + context = bot.Bot(0, "FIXME") # Load the prompt prmpt = prompt.Prompt() diff --git a/networkbot.py b/networkbot.py index 53e886b..3624efc 100644 --- a/networkbot.py +++ b/networkbot.py @@ -217,6 +217,9 @@ class NetworkBot: elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested self.context.receive_message(self, args[0].encode(), True, tag) + elif (cmd == b'NOMORE' or cmd == b'"NOMORE"') and len(args) > 0: # Reset !more feature + if args[0] in self.srv.moremessages: + del self.srv.moremessages[args[0]] def exec_hook(self, msg): self.send_cmd(["HOOK", msg.raw]) diff --git a/response.py b/response.py index f4cb09c..8d41a3b 100644 --- a/response.py +++ b/response.py @@ -22,12 +22,14 @@ import sys class Response: def __init__(self, sender, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", count=None, - shown_first_count=-1): + ctcp=False, shown_first_count=-1): self.nomore = nomore self.more = more self.rawtitle = title + self.server = server self.messages = list() self.alone = True + self.ctcp = ctcp if message is not None: self.append_message(message, shown_first_count=shown_first_count) self.elt = 0 # Next element to display diff --git a/server.py b/server.py index 479d2e4..67bf0bc 100644 --- a/server.py +++ b/server.py @@ -16,37 +16,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import errno -import os import socket import sys import threading -import traceback - -import channel -import DCC -import message -import xmlparser class Server(threading.Thread): - def __init__(self, node, nick, owner, realname, socket = None): + def __init__(self, socket = None): self.stop = False self.stopping = threading.Event() - self.nick = nick - self.owner = owner - self.realname = realname self.s = socket self.connected = self.s is not None - self.node = node - - self.listen_nick = True - - self.dcc_clients = dict() - - self.channels = dict() - for chn in node.getNodes("channel"): - chan = channel.Channel(chn, self) - self.channels[chan.name] = chan self.moremessages = dict() @@ -55,41 +34,17 @@ class Server(threading.Thread): def isDCC(self, to=None): return to is not None and to in self.dcc_clients - @property - def host(self): - if self.node.hasAttribute("server"): - return self.node["server"] - else: - return "localhost" - - @property - def port(self): - if self.node.hasAttribute("port"): - return self.node.getInt("port") - else: - return "6667" - - @property - def password(self): - if self.node.hasAttribute("password"): - return self.node["password"] - else: - return None - - @property - def allow_all(self): - return self.node.hasAttribute("allowall") and self.node["allowall"] == "true" - @property def ip(self): """Convert common IP representation to little-endian integer representation""" sum = 0 if self.node.hasAttribute("ip"): - for b in self.node["ip"].split("."): - sum = 256 * sum + int(b) + ip = self.node["ip"] else: #TODO: find the external IP - pass + ip = "0.0.0.0" + for b in ip.split("."): + sum = 256 * sum + int(b) return sum def toIP(self, input): @@ -101,27 +56,22 @@ class Server(threading.Thread): input = (input - mod) / 256 return ip[:len(ip) - 1] - @property - def autoconnect(self): - if self.node.hasAttribute("autoconnect"): - value = self.node["autoconnect"].lower() - return value != "no" and value != "off" and value != "false" - else: - return False - @property def id(self): - return self.host + ":" + str(self.port) - - def send_pong(self, cnt): - self.s.send(("PONG %s\r\n" % cnt).encode ()) + """Gives the server identifiant""" + raise NotImplemented() def msg_treated(self, origin): - """Do nothing, here for implement abstract class""" - pass + """Action done on server when a message was treated""" + raise NotImplemented() def send_response(self, res, origin): - if res.channel is not None and res.channel != self.nick: + """Analyse a Response and send it""" + # TODO: how to send a CTCP message to a different person + if res.ctcp: + self.send_ctcp(res.sender, res.get_message()) + + elif res.channel is not None and res.channel != self.nick: self.send_msg(res.channel, res.get_message()) if not res.alone: @@ -132,188 +82,74 @@ class Server(threading.Thread): if not res.alone: self.moremessages[res.sender] = res - def send_ctcp(self, to, msg, cmd = "NOTICE", endl = "\r\n"): + def send_ctcp(self, to, msg, cmd="NOTICE", endl="\r\n"): """Send a message as CTCP response""" if msg is not None and to is not None: for line in msg.split("\n"): - if self.s is None: - print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, msg)) - traceback.print_stack() - elif line != "": - self.s.send (("%s %s :\x01%s\x01%s" % (cmd, to.split("!")[0], line, endl)).encode ()) + if line != "": + self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl) def send_dcc(self, msg, to): """Send a message through DCC connection""" - if msg is not None and to is not None: - realname = to.split("!")[1] - if realname not in self.dcc_clients.keys(): - d = dcc.DCC(self, to) - self.dcc_clients[realname] = d - self.dcc_clients[realname].send_dcc(msg) + raise NotImplemented() - - def send_msg_final(self, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message without checks""" - if channel == self.nick: - print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg) - traceback.print_stack() - if msg is not None and channel is not None: - for line in msg.split("\n"): - if line != "": - if self.s is None: - print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, msg)) - traceback.print_stack() - elif len(line) < 442: - self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) - else: - self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ()) + def send_msg_final(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): + """Send a message without checks or format""" + raise NotImplemented() def send_msg_usr(self, user, msg): """Send a message to a user instead of a channel""" - if user is not None and user[0] != "#": - realname = user.split("!")[1] - if realname in self.dcc_clients: - self.send_dcc(msg, user) - else: - self.send_msg_final(user.split('!')[0], msg) + raise NotImplemented() - def send_msg(self, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): + def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): """Send a message to a channel""" - if self.accepted_channel(channel): - self.send_msg_final(channel, msg, cmd, endl) + if msg is not None: + for line in msg.split("\n"): + if line != "": + self.send_msg_final(channel, line, cmd, endl) - def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message to a channel, only if the source user is on this channel too""" - if self.accepted_channel(channel, sender): - self.send_msg_final(channel, msg, cmd, endl) + def send_msg_verified(self, sender, channel, msg, cmd="PRIVMSG", endl="\r\n"): + """A more secure way to send messages""" + raise NotImplemented() - def send_global(self, msg, cmd = "PRIVMSG", endl = "\r\n"): + def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"): """Send a message to all channels on this server""" - for channel in self.channels.keys(): - self.send_msg(channel, msg, cmd, endl) - - - def accepted_channel(self, chan, sender = None): - """Return True if the channel (or the user) is authorized""" - if self.allow_all: - return True - elif self.listen_nick: - return (chan in self.channels and (sender is None or sender in - self.channels[chan].people) - ) or chan == self.nick - else: - return chan in self.channels and (sender is None or sender - in self.channels[chan].people) + raise NotImplemented() def disconnect(self): - """Close the socket with the server and all DCC client connections""" + """Close the socket with the server""" if self.connected: self.stop = True self.s.shutdown(socket.SHUT_RDWR) - #Close all DCC connection - for clt in self.dcc_clients: - self.dcc_clients[clt].disconnect() - self.stopping.wait() return True else: return False def kill(self): + """Just stop the main loop, don't close the socket directly""" if self.connected: self.stop = True self.connected = False #Send a message in order to close the socket - self.s.send(("WHO %s\r\n" % self.nick).encode ()) + self.s.send(("Bye!\r\n" % self.nick).encode ()) self.stopping.wait() return True else: return False - def join(self, chan, password = None): - """Join a channel""" - if chan is not None and self.connected and chan not in self.channels: - chn = xmlparser.module_state.ModuleState("channel") - chn["name"] = chan - chn["password"] = password - self.node.addChild(chn) - self.channels[chan] = channel.Channel(chn, self) - if password is not None: - self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode ()) - else: - self.s.send(("JOIN %s\r\n" % chan).encode ()) - return True - else: - return False - - def leave(self, chan): - """Leave a channel""" - if chan is not None and self.connected and chan in self.channels: - self.s.send(("PART %s\r\n" % self.channels[chan].name).encode ()) - del self.channels[chan] - return True - else: - return False - - def launch(self, context, verb=True): + def launch(self, receive_action, verb=True): """Connect to the server if it is no yet connected""" - self.context = context + self._receive_action = receive_action if not self.connected: self.stop = False self.start() elif verb: print (" Already connected.") - def treat_msg(self, line, private = False): - self.context.receive_message(self, line, private) + def treat_msg(self, line, private=False): + self._receive_action(self, line, private) def run(self): - if not self.connected: - self.s = socket.socket() #Create the socket - try: - self.s.connect((self.host, self.port)) #Connect to server - except socket.error as e: - self.s = None - print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s" - % (self.host, self.port, os.strerror(e.errno))) - return - self.stopping.clear() - self.connected = True - - if self.password != None: - self.s.send(b"PASS " + self.password.encode () + b"\r\n") - self.s.send(("NICK %s\r\n" % self.nick).encode ()) - self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host, - self.realname)).encode ()) - print ("Connection to %s:%d completed" % (self.host, self.port)) - - if len(self.channels) > 0: - for chn in self.channels.keys(): - if self.channels[chn].password is not None: - self.s.send(("JOIN %s %s\r\n" % (self.channels[chn].name, - self.channels[chn].password)).encode ()) - else: - self.s.send(("JOIN %s\r\n" % - self.channels[chn].name).encode ()) - print ("Listen to channels: %s" % ' '.join (self.channels.keys())) - - readbuffer = b'' #Here we store all the messages from server - while not self.stop: - raw = self.s.recv(1024) #recieve server messages - if not raw: - break - readbuffer = readbuffer + raw - temp = readbuffer.split(b'\n') - readbuffer = temp.pop() - - for line in temp: - self.treat_msg(line) - - if self.connected: - self.s.close() - self.connected = False - print ("Server `%s' successfully stopped." % self.id) - self.stopping.set() - #Rearm Thread - threading.Thread.__init__(self) + raise NotImplemented()