Change add_server behaviour, fix IRC parameters parsing, can use with Python statement for managing server scope
This commit is contained in:
parent
f9ee1fe898
commit
4dd837cf4b
8 changed files with 248 additions and 67 deletions
117
server/IRC.py
117
server/IRC.py
|
|
@ -26,37 +26,46 @@ from channel import Channel
|
|||
import message
|
||||
from message.printer.IRC import IRC as IRCPrinter
|
||||
from server.socket import SocketServer
|
||||
import tools
|
||||
|
||||
class IRC(SocketServer):
|
||||
|
||||
def __init__(self, node, nick, owner, realname):
|
||||
self.id = nick + "@" + node["host"] + ":" + node["port"]
|
||||
self.printer = IRCPrinter
|
||||
SocketServer.__init__(self,
|
||||
node["host"],
|
||||
node["port"],
|
||||
node["password"],
|
||||
node.hasAttribute("ssl") and node["ssl"].lower() == "true")
|
||||
def __init__(self, owner, nick="nemubot", host="localhost", port=6667,
|
||||
ssl=False, password=None, realname="Nemubot",
|
||||
encoding="utf-8", caps=None, channels=list(),
|
||||
on_connect=None):
|
||||
"""Prepare a connection with an IRC server
|
||||
|
||||
Keyword arguments:
|
||||
owner -- bot's owner
|
||||
nick -- bot's nick
|
||||
host -- host to join
|
||||
port -- port on the host to reach
|
||||
ssl -- is this server using a TLS socket
|
||||
password -- if a password is required to connect to the server
|
||||
realname -- the bot's realname
|
||||
encoding -- the encoding used on the whole server
|
||||
caps -- client capabilities to register on the server
|
||||
channels -- list of channels to join on connection (if a channel is password protected, give a tuple: (channel_name, password))
|
||||
on_connect -- generator to call when connection is done
|
||||
"""
|
||||
|
||||
self.id = nick + "@" + host + ":" + port
|
||||
self.printer = IRCPrinter
|
||||
SocketServer.__init__(self, host=host, port=port, ssl=ssl)
|
||||
|
||||
self.password = password
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.realname = realname
|
||||
|
||||
#Keep a list of connected channels
|
||||
self.encoding = encoding
|
||||
|
||||
# Keep a list of joined channels
|
||||
self.channels = dict()
|
||||
|
||||
if node.hasAttribute("encoding"):
|
||||
self.encoding = node["encoding"]
|
||||
else:
|
||||
self.encoding = "utf-8"
|
||||
|
||||
if node.hasAttribute("caps"):
|
||||
if node["caps"].lower() == "no":
|
||||
self.capabilities = None
|
||||
else:
|
||||
self.capabilities = node["caps"].split(",")
|
||||
else:
|
||||
self.capabilities = list()
|
||||
# Server/client capabilities
|
||||
self.capabilities = caps
|
||||
|
||||
# Register CTCP capabilities
|
||||
self.ctcp_capabilities = dict()
|
||||
|
|
@ -68,7 +77,7 @@ class IRC(SocketServer):
|
|||
def _ctcp_dcc(msg, cmds):
|
||||
"""Response to DCC CTCP message"""
|
||||
try:
|
||||
ip = srv.toIP(int(cmds[3]))
|
||||
ip = tools.toIP(int(cmds[3]))
|
||||
port = int(cmds[4])
|
||||
conn = DCC(srv, msg.sender)
|
||||
except:
|
||||
|
|
@ -98,6 +107,7 @@ class IRC(SocketServer):
|
|||
|
||||
self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
|
||||
|
||||
|
||||
# Register hooks on some IRC CMD
|
||||
self.hookscmd = dict()
|
||||
|
||||
|
|
@ -109,14 +119,15 @@ class IRC(SocketServer):
|
|||
# Respond to 001
|
||||
def _on_connect(msg):
|
||||
# First, send user defined command
|
||||
if node.hasAttribute("on_connect"):
|
||||
self.write(node["on_connect"])
|
||||
if on_connect is not None:
|
||||
for oc in on_connect():
|
||||
self.write(oc)
|
||||
# Then, JOIN some channels
|
||||
for chn in node.getNodes("channel"):
|
||||
if chn["password"] is not None:
|
||||
self.write("JOIN %s %s" % (chn["name"], chn["password"]))
|
||||
for chn in channels:
|
||||
if isinstance(chn, tuple):
|
||||
self.write("JOIN %s %s" % chn)
|
||||
else:
|
||||
self.write("JOIN %s" % chn["name"])
|
||||
self.write("JOIN %s" % chn)
|
||||
self.hookscmd["001"] = _on_connect
|
||||
|
||||
# Respond to ERROR
|
||||
|
|
@ -141,9 +152,9 @@ class IRC(SocketServer):
|
|||
def _on_join(msg):
|
||||
if len(msg.params) == 0: return
|
||||
|
||||
for chname in msg.params[0].split(b","):
|
||||
for chname in msg.decode(msg.params[0]).split(","):
|
||||
# Register the channel
|
||||
chan = Channel(msg.decode(chname))
|
||||
chan = Channel(chname)
|
||||
self.channels[chname] = chan
|
||||
self.hookscmd["JOIN"] = _on_join
|
||||
# Respond to PART
|
||||
|
|
@ -197,6 +208,8 @@ class IRC(SocketServer):
|
|||
self.hookscmd["PRIVMSG"] = _on_ctcp
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def _open(self):
|
||||
if SocketServer._open(self):
|
||||
if self.password is not None:
|
||||
|
|
@ -214,6 +227,10 @@ class IRC(SocketServer):
|
|||
return SocketServer._close(self)
|
||||
|
||||
|
||||
# Writes: as inherited
|
||||
|
||||
# Read
|
||||
|
||||
def read(self):
|
||||
for line in SocketServer.read(self):
|
||||
msg = IRCMessage(line, self.encoding)
|
||||
|
|
@ -226,6 +243,8 @@ class IRC(SocketServer):
|
|||
yield mes
|
||||
|
||||
|
||||
# Parsing stuff
|
||||
|
||||
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
|
||||
(?::(?P<prefix>
|
||||
(?P<nick>[^!@ ]+)
|
||||
|
|
@ -269,7 +288,7 @@ class IRCMessage:
|
|||
self.cmd = self.decode(p.group("command"))
|
||||
|
||||
# Parse params
|
||||
if p.group("params") is not None:
|
||||
if p.group("params") is not None and p.group("params") != b'':
|
||||
for param in p.group("params").strip().split(b' '):
|
||||
self.params.append(param)
|
||||
|
||||
|
|
@ -278,7 +297,13 @@ class IRCMessage:
|
|||
|
||||
|
||||
def add_tag(self, key, value=None):
|
||||
"""Add an IRCv3.2 Message Tags"""
|
||||
"""Add an IRCv3.2 Message Tags
|
||||
|
||||
Arguments:
|
||||
key -- tag identifier (unique for the message)
|
||||
value -- optional value for the tag
|
||||
"""
|
||||
|
||||
# Treat special tags
|
||||
if key == "time":
|
||||
value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc)
|
||||
|
|
@ -289,11 +314,17 @@ class IRCMessage:
|
|||
|
||||
@property
|
||||
def is_ctcp(self):
|
||||
"""Analyze a message, to determine if this is a CTCP one"""
|
||||
return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01)
|
||||
|
||||
|
||||
def decode(self, s):
|
||||
"""Decode the content string usign a specific encoding"""
|
||||
"""Decode the content string usign a specific encoding
|
||||
|
||||
Argument:
|
||||
s -- string to decode
|
||||
"""
|
||||
|
||||
if isinstance(s, bytes):
|
||||
try:
|
||||
s = s.decode()
|
||||
|
|
@ -326,6 +357,12 @@ class IRCMessage:
|
|||
|
||||
|
||||
def to_message(self, srv):
|
||||
"""Convert to one of concrete implementation of AbstractMessage
|
||||
|
||||
Argument:
|
||||
srv -- the server from the message was received
|
||||
"""
|
||||
|
||||
if self.cmd == "PRIVMSG" or self.cmd == "NOTICE":
|
||||
|
||||
receivers = self.decode(self.params[0]).split(',')
|
||||
|
|
@ -344,6 +381,13 @@ class IRCMessage:
|
|||
else:
|
||||
text = self.decode(self.params[1])
|
||||
|
||||
if text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
|
||||
designated = srv.nick
|
||||
text = text[len(srv.nick) + 1:].strip()
|
||||
else:
|
||||
designated = None
|
||||
|
||||
# Is this a command?
|
||||
if len(text) > 1 and text[0] == '!':
|
||||
text = text[1:].strip()
|
||||
|
||||
|
|
@ -355,10 +399,11 @@ class IRCMessage:
|
|||
|
||||
return message.Command(cmd=args[0], args=args[1:], **common_args)
|
||||
|
||||
elif text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
|
||||
text = text[len(srv.nick) + 1:].strip()
|
||||
return message.DirectAsk(designated=srv.nick, message=text, **common_args)
|
||||
# Is this an ask for this bot?
|
||||
elif designated is not None:
|
||||
return message.DirectAsk(designated=designated, message=text, **common_args)
|
||||
|
||||
# Normal message
|
||||
else:
|
||||
return message.TextMessage(message=text, **common_args)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ class AbstractServer(io.IOBase):
|
|||
send_callback -- Callback when developper want to send a message
|
||||
"""
|
||||
|
||||
if not hasattr(self, "id"):
|
||||
raise Exception("No id defined for this server. Please set one!")
|
||||
|
||||
self.logger = logging.getLogger("nemubot.server." + self.id)
|
||||
self._sending_queue = queue.Queue()
|
||||
if send_callback is not None:
|
||||
|
|
@ -46,8 +49,20 @@ class AbstractServer(io.IOBase):
|
|||
self._send_callback = self._write_select
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def __enter__(self):
|
||||
self.open()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
def open(self):
|
||||
"""Generic open function that register the server un _rlist in case of successful _open"""
|
||||
self.logger.info("Opening connection to %s", self.id)
|
||||
if self._open():
|
||||
_rlist.append(self)
|
||||
_xlist.append(self)
|
||||
|
|
@ -55,6 +70,7 @@ class AbstractServer(io.IOBase):
|
|||
|
||||
def close(self):
|
||||
"""Generic close function that register the server un _{r,w,x}list in case of successful _close"""
|
||||
self.logger.info("Closing connection to %s", self.id)
|
||||
if self._close():
|
||||
if self in _rlist:
|
||||
_rlist.remove(self)
|
||||
|
|
@ -64,10 +80,18 @@ class AbstractServer(io.IOBase):
|
|||
_xlist.remove(self)
|
||||
|
||||
|
||||
# Writes
|
||||
|
||||
def write(self, message):
|
||||
"""Send a message to the server using send_callback"""
|
||||
"""Asynchronymously send a message to the server using send_callback
|
||||
|
||||
Argument:
|
||||
message -- message to send
|
||||
"""
|
||||
|
||||
self._send_callback(message)
|
||||
|
||||
|
||||
def write_select(self):
|
||||
"""Internal function used by the select function"""
|
||||
try:
|
||||
|
|
@ -79,20 +103,27 @@ class AbstractServer(io.IOBase):
|
|||
except queue.Empty:
|
||||
pass
|
||||
|
||||
|
||||
def _write_select(self, message):
|
||||
"""Send a message to the server safely through select"""
|
||||
"""Send a message to the server safely through select
|
||||
|
||||
Argument:
|
||||
message -- message to send
|
||||
"""
|
||||
|
||||
self._sending_queue.put(self.format(message))
|
||||
self.logger.debug("Message '%s' appended to Queue", message)
|
||||
if self not in _wlist:
|
||||
_wlist.append(self)
|
||||
|
||||
def exception(self):
|
||||
"""Exception occurs in fd"""
|
||||
print("Unhandle file descriptor exception on server " + self.id)
|
||||
|
||||
|
||||
def send_response(self, response):
|
||||
"""Send a formated Message class"""
|
||||
"""Send a formated Message class
|
||||
|
||||
Argument:
|
||||
response -- message to send
|
||||
"""
|
||||
|
||||
if response is None:
|
||||
return
|
||||
|
||||
|
|
@ -104,3 +135,11 @@ class AbstractServer(io.IOBase):
|
|||
vprnt = self.printer()
|
||||
response.accept(vprnt)
|
||||
self.write(vprnt.pp)
|
||||
|
||||
|
||||
# Exceptions
|
||||
|
||||
def exception(self):
|
||||
"""Exception occurs in fd"""
|
||||
self.logger.warning("Unhandle file descriptor exception on server %s",
|
||||
self.id)
|
||||
|
|
|
|||
|
|
@ -23,23 +23,28 @@ from server import AbstractServer
|
|||
|
||||
class SocketServer(AbstractServer):
|
||||
|
||||
def __init__(self, host, port=6667, password=None, ssl=False):
|
||||
def __init__(self, host, port, ssl=False):
|
||||
AbstractServer.__init__(self)
|
||||
self.host = host
|
||||
self.port = int(port)
|
||||
self.password = password
|
||||
self.ssl = ssl
|
||||
|
||||
self.socket = None
|
||||
self.readbuffer = b''
|
||||
|
||||
|
||||
def fileno(self):
|
||||
return self.socket.fileno() if self.socket else None
|
||||
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Indicator of the connection aliveness"""
|
||||
return self.socket is not None
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def _open(self):
|
||||
# Create the socket
|
||||
self.socket = socket.socket()
|
||||
|
|
@ -60,6 +65,7 @@ class SocketServer(AbstractServer):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def _close(self):
|
||||
self._sending_queue.join()
|
||||
if self.connected:
|
||||
|
|
@ -71,18 +77,27 @@ class SocketServer(AbstractServer):
|
|||
self.socket = None
|
||||
return True
|
||||
|
||||
|
||||
# Write
|
||||
|
||||
def _write(self, cnt):
|
||||
if not self.connected: return
|
||||
|
||||
self.socket.send(cnt)
|
||||
|
||||
|
||||
def format(self, txt):
|
||||
if isinstance(txt, bytes):
|
||||
return txt + b'\r\n'
|
||||
else:
|
||||
return txt.encode() + b'\r\n'
|
||||
|
||||
|
||||
# Read
|
||||
|
||||
def read(self):
|
||||
if not self.connected: return
|
||||
|
||||
raw = self.socket.recv(1024)
|
||||
temp = (self.readbuffer + raw).split(b'\r\n')
|
||||
self.readbuffer = temp.pop()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue