Parse most of IRC messages in server part instead of core
This commit is contained in:
parent
db22436e5d
commit
7dc3b55c34
3 changed files with 127 additions and 87 deletions
87
message.py
87
message.py
|
@ -17,82 +17,45 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from response import Response
|
from response import Response
|
||||||
|
|
||||||
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
|
|
||||||
(?::(?P<prefix>
|
|
||||||
(?P<nick>[^!@ ]+)
|
|
||||||
(?: !(?P<user>[^@ ]+))?
|
|
||||||
(?:@(?P<host>[^ ]+))?
|
|
||||||
)\ )?
|
|
||||||
(?P<command>(?:[a-zA-Z]+|[0-9]{3}))
|
|
||||||
(?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))?
|
|
||||||
$''', re.X)
|
|
||||||
|
|
||||||
class Message:
|
class Message:
|
||||||
def __init__ (self, raw_line, timestamp, private=False):
|
def __init__ (self, orig, private=False):
|
||||||
self.raw = raw_line
|
self.cmd = orig.cmd
|
||||||
|
self.tags = orig.tags
|
||||||
|
self.params = orig.params
|
||||||
self.private = private
|
self.private = private
|
||||||
self.tags = { 'time': timestamp }
|
self.prefix = orig.prefix
|
||||||
self.params = list()
|
self.nick = orig.nick
|
||||||
|
|
||||||
p = mgx.match(raw_line.rstrip())
|
|
||||||
|
|
||||||
# Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee
|
|
||||||
if p.group("tags"):
|
|
||||||
for tgs in self.decode(p.group("tags")).split(';'):
|
|
||||||
tag = tgs.split('=')
|
|
||||||
if len(tag) > 1:
|
|
||||||
self.add_tag(tag[0], tag[1])
|
|
||||||
else:
|
|
||||||
self.add_tag(tag[0])
|
|
||||||
|
|
||||||
# Parse prefix if exists: :nick!user@host.com
|
|
||||||
self.prefix = self.decode(p.group("prefix"))
|
|
||||||
self.nick = self.decode(p.group("nick"))
|
|
||||||
self.user = self.decode(p.group("user"))
|
|
||||||
self.host = self.decode(p.group("host"))
|
|
||||||
|
|
||||||
# Parse command
|
|
||||||
self.cmd = self.decode(p.group("command"))
|
|
||||||
|
|
||||||
# Parse params
|
|
||||||
if p.group("params"):
|
|
||||||
for param in p.group("params").strip().split(b' '):
|
|
||||||
self.params.append(param)
|
|
||||||
|
|
||||||
if p.group("trailing"):
|
|
||||||
self.params.append(p.group("trailing"))
|
|
||||||
|
|
||||||
# Special commands
|
# Special commands
|
||||||
if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE':
|
if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE':
|
||||||
self.receivers = self.decode(self.params[0]).split(',')
|
self.receivers = orig.decode(self.params[0]).split(',')
|
||||||
|
|
||||||
# If CTCP, remove 0x01
|
# If CTCP, remove 0x01
|
||||||
if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01):
|
if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01):
|
||||||
self.is_ctcp = True
|
self.is_ctcp = True
|
||||||
self.text = self.decode(self.params[1][1:len(self.params[1])-1])
|
self.text = orig.decode(self.params[1][1:len(self.params[1])-1])
|
||||||
else:
|
else:
|
||||||
self.is_ctcp = False
|
self.is_ctcp = False
|
||||||
self.text = self.decode(self.params[1])
|
self.text = orig.decode(self.params[1])
|
||||||
|
|
||||||
# Split content by words
|
# Split content by words
|
||||||
self.parse_content()
|
self.parse_content()
|
||||||
|
|
||||||
elif self.cmd == '353': # RPL_NAMREPLY
|
elif self.cmd == '353': # RPL_NAMREPLY
|
||||||
self.receivers = [ self.decode(self.params[0]) ]
|
self.receivers = [ orig.decode(self.params[0]) ]
|
||||||
self.nicks = self.decode(self.params[1]).split(" ")
|
self.nicks = orig.decode(self.params[1]).split(" ")
|
||||||
|
|
||||||
elif self.cmd == '332':
|
elif self.cmd == '332':
|
||||||
self.receivers = [ self.decode(self.params[0]) ]
|
self.receivers = [ orig.decode(self.params[0]) ]
|
||||||
self.topic = self.decode(self.params[1]).split(" ")
|
self.topic = orig.decode(self.params[1]).split(" ")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for i in range(0, len(self.params)):
|
for i in range(0, len(self.params)):
|
||||||
self.params[i] = self.decode(self.params[i])
|
self.params[i] = orig.decode(self.params[i])
|
||||||
|
|
||||||
|
|
||||||
# TODO: here for legacy content
|
# TODO: here for legacy content
|
||||||
|
@ -116,25 +79,3 @@ class Message:
|
||||||
self.cmds = shlex.split(self.text)
|
self.cmds = shlex.split(self.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.cmds = self.text.split(' ')
|
self.cmds = self.text.split(' ')
|
||||||
|
|
||||||
|
|
||||||
def add_tag(self, key, value=None):
|
|
||||||
"""Add an IRCv3.2 Message Tags"""
|
|
||||||
# Treat special tags
|
|
||||||
if key == "time":
|
|
||||||
# TODO: this is UTC timezone, nemubot works with local timezone
|
|
||||||
value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
||||||
|
|
||||||
# Store tag
|
|
||||||
self.tags[key] = value
|
|
||||||
|
|
||||||
|
|
||||||
def decode(self, s):
|
|
||||||
"""Decode the content string usign a specific encoding"""
|
|
||||||
if isinstance(s, bytes):
|
|
||||||
try:
|
|
||||||
s = s.decode()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
#TODO: use encoding from config file
|
|
||||||
s = s.decode('utf-8', 'replace')
|
|
||||||
return s
|
|
||||||
|
|
122
server/IRC.py
122
server/IRC.py
|
@ -17,6 +17,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
|
||||||
import bot
|
import bot
|
||||||
from message import Message
|
from message import Message
|
||||||
|
@ -96,7 +98,7 @@ class IRCServer(SocketServer):
|
||||||
self.hookscmd = dict()
|
self.hookscmd = dict()
|
||||||
|
|
||||||
def _on_ping(msg):
|
def _on_ping(msg):
|
||||||
self.write("%s :%s" % ("PONG", msg.params[0]))
|
self.write(b"PONG :" + msg.params[0])
|
||||||
self.hookscmd["PING"] = _on_ping
|
self.hookscmd["PING"] = _on_ping
|
||||||
|
|
||||||
def _on_connect(msg):
|
def _on_connect(msg):
|
||||||
|
@ -113,9 +115,9 @@ class IRCServer(SocketServer):
|
||||||
self.hookscmd["ERROR"] = _on_error
|
self.hookscmd["ERROR"] = _on_error
|
||||||
|
|
||||||
def _on_cap(msg):
|
def _on_cap(msg):
|
||||||
if len(msg.params) != 3 or msg.params[1] != "LS":
|
if len(msg.params) != 3 or msg.params[1] != b"LS":
|
||||||
return
|
return
|
||||||
server_caps = msg.params[2].split(" ")
|
server_caps = msg.params[2].decode().split(" ")
|
||||||
for cap in self.capabilities:
|
for cap in self.capabilities:
|
||||||
if cap not in server_caps:
|
if cap not in server_caps:
|
||||||
self.capabilities.remove(cap)
|
self.capabilities.remove(cap)
|
||||||
|
@ -153,24 +155,118 @@ class IRCServer(SocketServer):
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
for line in SocketServer.read(self):
|
for line in SocketServer.read(self):
|
||||||
msg = Message(line, datetime.now())
|
msg = IRCMessage(line, datetime.now())
|
||||||
|
|
||||||
if msg.cmd in self.hookscmd:
|
if msg.cmd in self.hookscmd:
|
||||||
self.hookscmd[msg.cmd](msg)
|
self.hookscmd[msg.cmd](msg)
|
||||||
|
|
||||||
elif msg.cmd == "PRIVMSG" and msg.is_ctcp:
|
|
||||||
if msg.cmds[0] in self.ctcp_capabilities:
|
|
||||||
res = self.ctcp_capabilities[msg.cmds[0]](msg)
|
|
||||||
else:
|
|
||||||
res = _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request")
|
|
||||||
if res is not None:
|
|
||||||
self.send_response(res)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
yield msg
|
mes = msg.to_message()
|
||||||
|
mes.raw = msg.raw
|
||||||
|
|
||||||
|
if mes.cmd == "PRIVMSG" and mes.is_ctcp:
|
||||||
|
if mes.cmds[0] in self.ctcp_capabilities:
|
||||||
|
res = self.ctcp_capabilities[mes.cmds[0]](mes)
|
||||||
|
else:
|
||||||
|
res = _ctcp_response(mes.sender, "ERRMSG Unknown or unimplemented CTCP request")
|
||||||
|
if res is not None:
|
||||||
|
self.send_response(res)
|
||||||
|
|
||||||
|
else:
|
||||||
|
yield mes
|
||||||
|
|
||||||
|
|
||||||
from response import Response
|
from response import Response
|
||||||
|
|
||||||
def _ctcp_response(sndr, msg):
|
def _ctcp_response(sndr, msg):
|
||||||
return Response(sndr, msg, ctcp=True)
|
return Response(sndr, msg, ctcp=True)
|
||||||
|
|
||||||
|
|
||||||
|
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
|
||||||
|
(?::(?P<prefix>
|
||||||
|
(?P<nick>[^!@ ]+)
|
||||||
|
(?: !(?P<user>[^@ ]+))?
|
||||||
|
(?:@(?P<host>[^ ]+))?
|
||||||
|
)\ )?
|
||||||
|
(?P<command>(?:[a-zA-Z]+|[0-9]{3}))
|
||||||
|
(?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))?
|
||||||
|
$''', re.X)
|
||||||
|
|
||||||
|
class IRCMessage:
|
||||||
|
|
||||||
|
def __init__(self, raw, timestamp):
|
||||||
|
self.raw = raw
|
||||||
|
self.tags = { 'time': timestamp }
|
||||||
|
self.params = list()
|
||||||
|
|
||||||
|
p = mgx.match(raw.rstrip())
|
||||||
|
|
||||||
|
if p is None:
|
||||||
|
raise Exception("Not a valid IRC message: %s" % raw)
|
||||||
|
|
||||||
|
# Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee
|
||||||
|
if p.group("tags"):
|
||||||
|
for tgs in self.decode(p.group("tags")).split(';'):
|
||||||
|
tag = tgs.split('=')
|
||||||
|
if len(tag) > 1:
|
||||||
|
self.add_tag(tag[0], tag[1])
|
||||||
|
else:
|
||||||
|
self.add_tag(tag[0])
|
||||||
|
|
||||||
|
# Parse prefix if exists: :nick!user@host.com
|
||||||
|
self.prefix = self.decode(p.group("prefix"))
|
||||||
|
self.nick = self.decode(p.group("nick"))
|
||||||
|
self.user = self.decode(p.group("user"))
|
||||||
|
self.host = self.decode(p.group("host"))
|
||||||
|
|
||||||
|
# Parse command
|
||||||
|
self.cmd = self.decode(p.group("command"))
|
||||||
|
|
||||||
|
# Parse params
|
||||||
|
if p.group("params"):
|
||||||
|
for param in p.group("params").strip().split(b' '):
|
||||||
|
self.params.append(param)
|
||||||
|
|
||||||
|
if p.group("trailing"):
|
||||||
|
self.params.append(p.group("trailing"))
|
||||||
|
|
||||||
|
|
||||||
|
def add_tag(self, key, value=None):
|
||||||
|
"""Add an IRCv3.2 Message Tags"""
|
||||||
|
# Treat special tags
|
||||||
|
if key == "time":
|
||||||
|
# TODO: this is UTC timezone, nemubot works with local timezone
|
||||||
|
value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
|
|
||||||
|
# Store tag
|
||||||
|
self.tags[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def decode(self, s):
|
||||||
|
"""Decode the content string usign a specific encoding"""
|
||||||
|
if isinstance(s, bytes):
|
||||||
|
try:
|
||||||
|
s = s.decode()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
#TODO: use encoding from config file
|
||||||
|
s = s.decode('utf-8', 'replace')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def to_message(self):
|
||||||
|
return Message(self)
|
||||||
|
|
||||||
|
def to_irc_string(self, client=True):
|
||||||
|
res = ";".join(["@%s=%s" % (k,v) for k, v in self.tags.items()])
|
||||||
|
|
||||||
|
if not client: res += " :%s!%s@%s" % (self.nick, self.user, self.host)
|
||||||
|
|
||||||
|
res += " " + self.cmd
|
||||||
|
|
||||||
|
if len(self.params) > 0:
|
||||||
|
|
||||||
|
if len(self.params) > 1:
|
||||||
|
res += " " + " ".join(self.params[:-1])
|
||||||
|
res += " :" + self.params[-1]
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
|
@ -72,7 +72,10 @@ class SocketServer(AbstractServer):
|
||||||
self.socket.send(cnt)
|
self.socket.send(cnt)
|
||||||
|
|
||||||
def format(self, txt):
|
def format(self, txt):
|
||||||
return txt.encode() + b'\r\n'
|
if isinstance(txt, bytes):
|
||||||
|
return txt + b'\r\n'
|
||||||
|
else:
|
||||||
|
return txt.encode() + b'\r\n'
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
if self.socket is None: return
|
if self.socket is None: return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue