Introducing a new nemubot architecture for servers and messages treatment: messages have no more context or server

This commit is contained in:
Némunaire 2012-11-02 12:10:37 +01:00
parent f00dfc82f7
commit a62940380e
12 changed files with 471 additions and 327 deletions

53
DCC.py
View File

@ -26,18 +26,14 @@ import time
import traceback import traceback
import message import message
import server
#Store all used ports #Store all used ports
PORTS = list() PORTS = list()
class DCC(threading.Thread): class DCC(server.Server):
def __init__(self, srv, dest, socket=None): 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.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 self.messages = list() # Message queued before connexion
# Informations about the sender # Informations about the sender
@ -64,7 +60,7 @@ class DCC(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
def foundPort(self): def foundPort(self):
"""Found a free port for the connexion""" """Found a free port for the connection"""
for p in range(65432, 65535): for p in range(65432, 65535):
if p not in PORTS: if p not in PORTS:
PORTS.append(p) PORTS.append(p)
@ -73,41 +69,18 @@ class DCC(threading.Thread):
@property @property
def id(self): def id(self):
"""Gives the server identifiant"""
return self.srv.id + "/" + self.sender return self.srv.id + "/" + self.sender
def setError(self, msg): def setError(self, msg):
self.error = True self.error = True
self.srv.send_msg_usr(self.sender, msg) 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): def accept_user(self, host, port):
"""Accept a DCC connection""" """Accept a DCC connection"""
self.conn = socket.socket() self.s = socket.socket()
try: try:
self.conn.connect((host, port)) self.s.connect((host, port))
print ('Accepted user from', host, port, "for", self.sender) print ('Accepted user from', host, port, "for", self.sender)
self.connected = True self.connected = True
self.stop = False self.stop = False
@ -143,13 +116,13 @@ class DCC(threading.Thread):
s.listen(1) s.listen(1)
#Waiting for the client #Waiting for the client
(self.conn, addr) = s.accept() (self.s, addr) = s.accept()
print ('Connected by', addr) print ('Connected by', addr)
self.connected = True self.connected = True
return True return True
def send_dcc_raw(self, line): 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): def send_dcc(self, msg, to = None):
"""If we talk to this user, send a message through this connection """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 to is None or to == self.sender or to == self.nick:
if self.error: if self.error:
self.srv.send_msg_final(self.nick, msg) 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: if not self.DCC:
self.start() self.start()
self.DCC = True self.DCC = True
@ -190,8 +163,8 @@ class DCC(threading.Thread):
with open(self.messages, 'rb') as f: with open(self.messages, 'rb') as f:
d = f.read(268435456) #Packets size: 256Mo d = f.read(268435456) #Packets size: 256Mo
while d: while d:
self.conn.sendall(d) self.s.sendall(d)
self.conn.recv(4) #The client send a confirmation after each packet self.s.recv(4) #The client send a confirmation after each packet
d = f.read(268435456) #Packets size: 256Mo d = f.read(268435456) #Packets size: 256Mo
# Messages connection # Messages connection
@ -211,7 +184,7 @@ class DCC(threading.Thread):
self.nicksize = len(self.srv.nick) self.nicksize = len(self.srv.nick)
self.Bnick = self.srv.nick.encode() self.Bnick = self.srv.nick.encode()
while not self.stop: while not self.stop:
raw = self.conn.recv(1024) #recieve server messages raw = self.s.recv(1024) #recieve server messages
if not raw: if not raw:
break break
readbuffer = readbuffer + raw readbuffer = readbuffer + raw
@ -222,7 +195,7 @@ class DCC(threading.Thread):
self.treatement(line) self.treatement(line)
if self.connected: if self.connected:
self.conn.close() self.s.close()
self.connected = False self.connected = False
#Remove from DCC connections server list #Remove from DCC connections server list

263
IRCServer.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

67
bot.py
View File

@ -26,19 +26,26 @@ import consumer
import event import event
import hooks import hooks
from networkbot import NetworkBot from networkbot import NetworkBot
from server import Server from IRCServer import IRCServer
import response
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
class Bot: class Bot:
def __init__(self, servers=dict(), modules=dict(), mp=list()): def __init__(self, ip, realname, mp=list()):
# Bot general informations # Bot general informations
self.version = 3.2 self.version = 3.3
self.version_txt = "3.2-dev" 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 # Keep global context: servers and modules
self.servers = servers self.servers = dict()
self.modules = modules self.modules = dict()
# Context paths # Context paths
self.modules_path = mp self.modules_path = mp
@ -61,6 +68,36 @@ class Bot:
self.cnsr_thrd_size = -1 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): def add_event(self, evt, eid=None):
"""Register an event and return its identifiant for futur update""" """Register an event and return its identifiant for futur update"""
if eid is None: if eid is None:
@ -126,7 +163,7 @@ class Bot:
while len(self.events)>0 and datetime.now() >= self.events[0].current: while len(self.events)>0 and datetime.now() >= self.events[0].current:
#print ("end timer: while") #print ("end timer: while")
evt = self.events.pop(0) 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_consumers()
self.update_timer() self.update_timer()
@ -134,11 +171,11 @@ class Bot:
def addServer(self, node, nick, owner, realname): def addServer(self, node, nick, owner, realname):
"""Add a new server to the context""" """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: if srv.id not in self.servers:
self.servers[srv.id] = srv self.servers[srv.id] = srv
if srv.autoconnect: if srv.autoconnect:
srv.launch(self) srv.launch(self.receive_message)
return True return True
else: else:
return False return False
@ -296,6 +333,16 @@ class Bot:
self.check_rest_times(store, h) 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): def treat_cmd(self, msg):
"""Treat a command message""" """Treat a command message"""
treated = list() treated = list()
@ -401,6 +448,8 @@ class Bot:
return treated return treated
def _ctcp_response(sndr, msg):
return response.Response(sndr, msg, ctcp=True)
def hotswap(bak): def hotswap(bak):
return Bot(bak.servers, bak.modules, bak.modules_path) return Bot(bak.servers, bak.modules, bak.modules_path)

View File

@ -17,12 +17,15 @@
# 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 queue import queue
import re
import threading import threading
import traceback import traceback
import sys import sys
import bot
from message import Message from message import Message
from response import Response from response import Response
import server
class MessageConsumer: class MessageConsumer:
"""Store a message before treating""" """Store a message before treating"""
@ -33,11 +36,64 @@ class MessageConsumer:
self.prvt = prvt self.prvt = prvt
self.data = data self.data = data
def run(self): def treat_in(self, context, msg):
# Create, parse and treat the message """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: try:
msg = Message(self.srv, self.raw, self.time, self.prvt) msg = Message(self.srv, self.raw, self.time, self.prvt)
res = msg.treat() res = self.treat_in(context, msg)
except: except:
print ("\033[1;31mERROR:\033[0m occurred during the " print ("\033[1;31mERROR:\033[0m occurred during the "
"processing of the message: %s" % self.raw) "processing of the message: %s" % self.raw)
@ -51,26 +107,25 @@ class MessageConsumer:
if isinstance(res, list): if isinstance(res, list):
for r in res: for r in res:
if isinstance(r, Response): if isinstance(r, Response):
self.srv.send_response(r, self.data) self.treat_out(context, r)
elif isinstance(r, list): elif isinstance(r, list):
for s in r: for s in r:
if isinstance(s, Response): if isinstance(s, Response):
self.srv.send_response(s, self.data) self.treat_out(context, s)
elif isinstance(res, Response): elif isinstance(res, Response):
self.srv.send_response(res, self.data) self.treat_out(context, res)
# Inform that the message has been treated # Inform that the message has been treated
self.srv.msg_treated(self.data) self.srv.msg_treated(self.data)
class EventConsumer: class EventConsumer:
"""Store a event before treating""" """Store a event before treating"""
def __init__(self, context, evt, timeout=20): def __init__(self, evt, timeout=20):
self.context = context
self.evt = evt self.evt = evt
self.timeout = timeout self.timeout = timeout
def run(self): def run(self, context):
try: try:
self.evt.launch_check() self.evt.launch_check()
except: except:
@ -79,9 +134,9 @@ class EventConsumer:
traceback.print_exception(exc_type, exc_value, traceback.print_exception(exc_type, exc_value,
exc_traceback) exc_traceback)
if self.evt.next is not None: 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): class Consumer(threading.Thread):
"""Dequeue and exec requested action""" """Dequeue and exec requested action"""
@ -94,7 +149,7 @@ class Consumer(threading.Thread):
try: try:
while not self.stop: while not self.stop:
stm = self.context.cnsr_queue.get(True, 20) stm = self.context.cnsr_queue.get(True, 20)
stm.run() stm.run(self.context)
except queue.Empty: except queue.Empty:
pass pass

View File

@ -24,7 +24,7 @@ class MessagesHook:
# Store specials hook # Store specials hook
self.all_pre = list() # Treated before any parse 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 # Store direct hook
self.cmd_hook = dict() self.cmd_hook = dict()

View File

@ -136,7 +136,7 @@ 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 module.nemubotversion > self.context.version:
raise ImportError("Module `%s' is not compatible with this " raise ImportError("Module `%s' is not compatible with this "
"version." % self.name) "version." % self.name)

View File

@ -73,9 +73,17 @@ class Message:
self.channel = self.pickWords(words[2:]).decode() self.channel = self.pickWords(words[2:]).decode()
if self.cmd == 'PRIVMSG': if self.cmd == 'PRIVMSG':
#Check for CTCP request # Check for CTCP request
self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01) self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01)
self.content = self.pickWords(words[3:]) 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: elif self.cmd == '353' and len(words) > 3:
for i in range(2, len(words)): for i in range(2, len(words)):
if words[i][0] == 58: if words[i][0] == 58:
@ -125,8 +133,9 @@ class Message:
def is_owner(self): def is_owner(self):
return self.nick == self.srv.owner return self.nick == self.srv.owner
def authorize(self): def authorize_DEPRECATED(self):
"""Is nemubot listening for the sender on this channel?""" """Is nemubot listening for the sender on this channel?"""
# TODO: deprecated
if self.srv.isDCC(self.sender): if self.srv.isDCC(self.sender):
return True return True
elif self.realname not in CREDITS: elif self.realname not in CREDITS:
@ -139,13 +148,7 @@ class Message:
def treat(self): def treat(self):
"""Parse and treat the message""" """Parse and treat the message"""
if self.cmd == "PING": if self.channel in self.srv.channels:
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.cmd == "353": if self.cmd == "353":
self.srv.channels[self.channel].parse353(self) self.srv.channels[self.channel].parse353(self)
elif self.cmd == "332": elif self.cmd == "332":
@ -166,53 +169,13 @@ class Message:
self.srv.channels[chn].part(self.nick) self.srv.channels[chn].part(self.nick)
return None 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): def reparsemsg(self):
self.parsemsg() self.parsemsg()
def parsemsg (self): 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 #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(' ') self.cmd = self.content[1:].split(' ')
if self.cmd[0] == "ban": if self.cmd[0] == "ban":
if len(self.cmd) > 1: if len(self.cmd) > 1:

View File

@ -50,12 +50,12 @@ def connect(data, toks, context, prompt):
if len(toks) > 1: if len(toks) > 1:
for s in toks[1:]: for s in toks[1:]:
if s in context.servers: if s in context.servers:
context.servers[s].launch(context) context.servers[s].launch(context.receive_message)
else: else:
print ("connect: server `%s' not found." % s) print ("connect: server `%s' not found." % s)
elif prompt.selectedServer is not None: elif prompt.selectedServer is not None:
prompt.selectedServer.launch(context) prompt.selectedServer.launch(context.receive_message)
else: else:
print (" Please SELECT a server or give its name in argument.") print (" Please SELECT a server or give its name in argument.")

View File

@ -29,7 +29,7 @@ import importer
if __name__ == "__main__": if __name__ == "__main__":
# Create bot context # Create bot context
context = bot.Bot() context = bot.Bot(0, "FIXME")
# Load the prompt # Load the prompt
prmpt = prompt.Prompt() prmpt = prompt.Prompt()

View File

@ -217,6 +217,9 @@ class NetworkBot:
elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested
self.context.receive_message(self, args[0].encode(), True, tag) 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): def exec_hook(self, msg):
self.send_cmd(["HOOK", msg.raw]) self.send_cmd(["HOOK", msg.raw])

View File

@ -22,12 +22,14 @@ import sys
class Response: class Response:
def __init__(self, sender, message=None, channel=None, nick=None, server=None, def __init__(self, sender, message=None, channel=None, nick=None, server=None,
nomore="No more message", title=None, more="(suite) ", count=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.nomore = nomore
self.more = more self.more = more
self.rawtitle = title self.rawtitle = title
self.server = server
self.messages = list() self.messages = list()
self.alone = True self.alone = True
self.ctcp = ctcp
if message is not None: if message is not None:
self.append_message(message, shown_first_count=shown_first_count) self.append_message(message, shown_first_count=shown_first_count)
self.elt = 0 # Next element to display self.elt = 0 # Next element to display

246
server.py
View File

@ -16,37 +16,16 @@
# 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 errno
import os
import socket import socket
import sys import sys
import threading import threading
import traceback
import channel
import DCC
import message
import xmlparser
class Server(threading.Thread): class Server(threading.Thread):
def __init__(self, node, nick, owner, realname, socket = None): def __init__(self, socket = None):
self.stop = False self.stop = False
self.stopping = threading.Event() self.stopping = threading.Event()
self.nick = nick
self.owner = owner
self.realname = realname
self.s = socket self.s = socket
self.connected = self.s is not None 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() self.moremessages = dict()
@ -55,41 +34,17 @@ class Server(threading.Thread):
def isDCC(self, to=None): def isDCC(self, to=None):
return to is not None and to in self.dcc_clients 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 @property
def ip(self): def ip(self):
"""Convert common IP representation to little-endian integer representation""" """Convert common IP representation to little-endian integer representation"""
sum = 0 sum = 0
if self.node.hasAttribute("ip"): if self.node.hasAttribute("ip"):
for b in self.node["ip"].split("."): ip = self.node["ip"]
sum = 256 * sum + int(b)
else: else:
#TODO: find the external IP #TODO: find the external IP
pass ip = "0.0.0.0"
for b in ip.split("."):
sum = 256 * sum + int(b)
return sum return sum
def toIP(self, input): def toIP(self, input):
@ -101,27 +56,22 @@ class Server(threading.Thread):
input = (input - mod) / 256 input = (input - mod) / 256
return ip[:len(ip) - 1] 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 @property
def id(self): def id(self):
return self.host + ":" + str(self.port) """Gives the server identifiant"""
raise NotImplemented()
def send_pong(self, cnt):
self.s.send(("PONG %s\r\n" % cnt).encode ())
def msg_treated(self, origin): def msg_treated(self, origin):
"""Do nothing, here for implement abstract class""" """Action done on server when a message was treated"""
pass raise NotImplemented()
def send_response(self, res, origin): 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()) self.send_msg(res.channel, res.get_message())
if not res.alone: if not res.alone:
@ -132,188 +82,74 @@ class Server(threading.Thread):
if not res.alone: if not res.alone:
self.moremessages[res.sender] = res 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""" """Send a message as CTCP response"""
if msg is not None and to is not None: if msg is not None and to is not None:
for line in msg.split("\n"): for line in msg.split("\n"):
if self.s is None: if line != "":
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, msg)) self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl)
traceback.print_stack()
elif line != "":
self.s.send (("%s %s :\x01%s\x01%s" % (cmd, to.split("!")[0], line, endl)).encode ())
def send_dcc(self, msg, to): def send_dcc(self, msg, to):
"""Send a message through DCC connection""" """Send a message through DCC connection"""
if msg is not None and to is not None: raise NotImplemented()
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, msg, cmd="PRIVMSG", endl="\r\n"):
def send_msg_final(self, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): """Send a message without checks or format"""
"""Send a message without checks""" raise NotImplemented()
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_usr(self, user, msg): def send_msg_usr(self, user, msg):
"""Send a message to a user instead of a channel""" """Send a message to a user instead of a channel"""
if user is not None and user[0] != "#": raise NotImplemented()
realname = user.split("!")[1]
if realname in self.dcc_clients:
self.send_dcc(msg, user)
else:
self.send_msg_final(user.split('!')[0], msg)
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""" """Send a message to a channel"""
if self.accepted_channel(channel): if msg is not None:
self.send_msg_final(channel, msg, cmd, endl) 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"): 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""" """A more secure way to send messages"""
if self.accepted_channel(channel, sender): raise NotImplemented()
self.send_msg_final(channel, msg, cmd, endl)
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""" """Send a message to all channels on this server"""
for channel in self.channels.keys(): raise NotImplemented()
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)
def disconnect(self): def disconnect(self):
"""Close the socket with the server and all DCC client connections""" """Close the socket with the server"""
if self.connected: if self.connected:
self.stop = True self.stop = True
self.s.shutdown(socket.SHUT_RDWR) self.s.shutdown(socket.SHUT_RDWR)
#Close all DCC connection
for clt in self.dcc_clients:
self.dcc_clients[clt].disconnect()
self.stopping.wait() self.stopping.wait()
return True return True
else: else:
return False return False
def kill(self): def kill(self):
"""Just stop the main loop, don't close the socket directly"""
if self.connected: if self.connected:
self.stop = True self.stop = True
self.connected = False self.connected = False
#Send a message in order to close the socket #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() self.stopping.wait()
return True return True
else: else:
return False return False
def join(self, chan, password = None): def launch(self, receive_action, verb=True):
"""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):
"""Connect to the server if it is no yet connected""" """Connect to the server if it is no yet connected"""
self.context = context self._receive_action = receive_action
if not self.connected: if not self.connected:
self.stop = False self.stop = False
self.start() self.start()
elif verb: elif verb:
print (" Already connected.") print (" Already connected.")
def treat_msg(self, line, private = False): def treat_msg(self, line, private=False):
self.context.receive_message(self, line, private) self._receive_action(self, line, private)
def run(self): def run(self):
if not self.connected: raise NotImplemented()
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)