From a1ac7d480d07f9b678f974fd48d6746df6f163c6 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 13 Jul 2015 20:35:40 +0200 Subject: [PATCH] Split server message parsing from message retrieving --- nemubot/server/IRC.py | 176 +------------------------- nemubot/server/message/IRC.py | 190 +++++++++++++++++++++++++++++ nemubot/server/message/abstract.py | 35 ++++++ nemubot/server/test_IRC.py | 6 +- 4 files changed, 231 insertions(+), 176 deletions(-) create mode 100644 nemubot/server/message/IRC.py create mode 100644 nemubot/server/message/abstract.py diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 718a9bc..25f37cf 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -16,13 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from datetime import datetime, timezone +from datetime import datetime import re -import shlex from nemubot.channel import Channel -import nemubot.message as message from nemubot.message.printer.IRC import IRC as IRCPrinter +from nemubot.server.message.IRC import IRC as IRCMessage from nemubot.server.socket import SocketServer @@ -263,175 +262,6 @@ class IRC(SocketServer): if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) - mes = msg.to_message(self) + mes = msg.to_bot_message(self) if mes is not None: yield mes - - -# Parsing stuff - -mgx = re.compile(b'''^(?:@(?P[^ ]+)\ )? - (?::(?P - (?P[^!@ ]+) - (?: !(?P[^@ ]+))? - (?:@(?P[^ ]*))? - )\ )? - (?P(?:[a-zA-Z]+|[0-9]{3})) - (?P(?:\ [^:][^ ]*)*)(?:\ :(?P.*))? - $''', re.X) - -class IRCMessage: - - """Class responsible for parsing IRC messages""" - - def __init__(self, raw, encoding="utf-8"): - self.encoding = encoding - self.tags = { 'time': datetime.now(timezone.utc) } - 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") is not None and p.group("params") != b'': - for param in p.group("params").strip().split(b' '): - self.params.append(param) - - if p.group("trailing") is not None: - self.params.append(p.group("trailing")) - - - def add_tag(self, key, value=None): - """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" and value is not None: - import calendar, time - value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) - - # Store tag - self.tags[key] = value - - - @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 - - Argument: - s -- string to decode - """ - - if isinstance(s, bytes): - try: - s = s.decode() - except UnicodeDecodeError: - s = s.decode(self.encoding, 'replace') - return s - - - - def to_irc_string(self, client=True): - """Pretty print the message to close to original input string - - Keyword argument: - client -- export as a client-side string if true - """ - - res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) 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 += " " + self.decode(b" ".join(self.params[:-1])) - res += " :" + self.decode(self.params[-1]) - - return res - - - 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(',') - - common_args = { - "server": srv.id, - "date": self.tags["time"], - "to": receivers, - "to_response": [r if r != srv.nick else self.nick for r in receivers], - "frm": self.nick - } - - # If CTCP, remove 0x01 - if self.is_ctcp: - text = self.decode(self.params[1][1:len(self.params[1])-1]) - 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() - - # Split content by words - try: - args = shlex.split(text) - except ValueError: - args = text.split(' ') - - return message.Command(cmd=args[0], args=args[1:], **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.Text(message=text, **common_args) - - return None diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py new file mode 100644 index 0000000..43dcf49 --- /dev/null +++ b/nemubot/server/message/IRC.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from datetime import datetime, timezone +import re +import shlex + +import nemubot.message as message +from nemubot.server.message.abstract import Abstract + +mgx = re.compile(b'''^(?:@(?P[^ ]+)\ )? + (?::(?P + (?P[^!@ ]+) + (?: !(?P[^@ ]+))? + (?:@(?P[^ ]*))? + )\ )? + (?P(?:[a-zA-Z]+|[0-9]{3})) + (?P(?:\ [^:][^ ]*)*)(?:\ :(?P.*))? + $''', re.X) + +class IRC(Abstract): + + """Class responsible for parsing IRC messages""" + + def __init__(self, raw, encoding="utf-8"): + self.encoding = encoding + self.tags = { 'time': datetime.now(timezone.utc) } + 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") is not None and p.group("params") != b'': + for param in p.group("params").strip().split(b' '): + self.params.append(param) + + if p.group("trailing") is not None: + self.params.append(p.group("trailing")) + + + def add_tag(self, key, value=None): + """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" and value is not None: + import calendar, time + value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) + + # Store tag + self.tags[key] = value + + + @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 + + Argument: + s -- string to decode + """ + + if isinstance(s, bytes): + try: + s = s.decode() + except UnicodeDecodeError: + s = s.decode(self.encoding, 'replace') + return s + + + + def to_server_string(self, client=True): + """Pretty print the message to close to original input string + + Keyword argument: + client -- export as a client-side string if true + """ + + res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) 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 += " " + self.decode(b" ".join(self.params[:-1])) + res += " :" + self.decode(self.params[-1]) + + return res + + + def to_bot_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(',') + + common_args = { + "server": srv.id, + "date": self.tags["time"], + "to": receivers, + "to_response": [r if r != srv.nick else self.nick for r in receivers], + "frm": self.nick + } + + # If CTCP, remove 0x01 + if self.is_ctcp: + text = self.decode(self.params[1][1:len(self.params[1])-1]) + 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() + + # Split content by words + try: + args = shlex.split(text) + except ValueError: + args = text.split(' ') + + return message.Command(cmd=args[0], args=args[1:], **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.Text(message=text, **common_args) + + return None diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py new file mode 100644 index 0000000..35d7105 --- /dev/null +++ b/nemubot/server/message/abstract.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class Abstract: + + def to_bot_message(self, srv): + """Convert to one of concrete implementation of AbstractMessage + + Argument: + srv -- the server from the message was received + """ + + raise NotImplemented + + + def to_server_message(self): + """Pretty print the message to close to original input string + """ + + raise NotImplemented diff --git a/nemubot/server/test_IRC.py b/nemubot/server/test_IRC.py index e22d190..3553d6b 100644 --- a/nemubot/server/test_IRC.py +++ b/nemubot/server/test_IRC.py @@ -23,13 +23,13 @@ class TestIRCMessage(unittest.TestCase): def test_prettyprint(self): - bst1 = self.msg.to_irc_string(False) + bst1 = self.msg.to_server_message(False) msg2 = IRC.IRCMessage(bst1.encode()) - bst2 = msg2.to_irc_string(False) + bst2 = msg2.to_server_message(False) msg3 = IRC.IRCMessage(bst2.encode()) - bst3 = msg3.to_irc_string(False) + bst3 = msg3.to_server_message(False) self.assertEqual(bst2, bst3)