diff --git a/.gitmodules b/.gitmodules index 23cf4a0..d189a7e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "modules/nextstop/external"] path = modules/nextstop/external url = git://github.com/nbr23/NextStop.git +[submodule "wiki"] + path = wiki + url = git@github.com:nemunaire/nemubot.wiki.git diff --git a/IRCServer.py b/IRCServer.py index f354330..dc7f207 100644 --- a/IRCServer.py +++ b/IRCServer.py @@ -122,18 +122,43 @@ class IRCServer(server.Server): if msg.channel in self.channels: self.channels[msg.channel].treat(msg.cmd, msg) + + def filter_receivers(self, receivers, sender=None): + """Return a filtered list of authorized channels and users""" + if self.allow_all: + return receivers + + filtered = list() + for chan in receivers: + if ( + # Is the channel authorized? + (chan in self.channels and + # Is the sender on the channel? + ( + sender is None or + sender in self.channels[chan].people + )) or + # Accept private messages? + (self.listen_nick and chan == self.nick) + ): + filtered.append(chan) + + 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, force=False): """Join a channel""" if force or (chan is not None and diff --git a/consumer.py b/consumer.py index a443dca..96793b5 100644 --- a/consumer.py +++ b/consumer.py @@ -41,10 +41,12 @@ class MessageConsumer: def treat_in(self, context, msg): """Treat the input message""" if msg.cmd == "PING": - self.srv.send_pong(msg.content) + self.srv.send_pong(msg.params[0]) + else: # TODO: Manage credits - if msg.channel is None or self.srv.accepted_channel(msg.channel): + msg.receivers = self.srv.filter_receivers(msg.receivers) + if len(msg.receivers) == 0: # All messages context.treat_pre(msg, self.srv) diff --git a/message.py b/message.py index d14a16d..c8db4c5 100644 --- a/message.py +++ b/message.py @@ -31,7 +31,7 @@ filename = "" def load(config_file): global CREDITS, filename - CREDITS = dict () + CREDITS = dict() filename = config_file credits.BANLIST = xmlparser.parse_file(filename) @@ -39,99 +39,107 @@ def save(): global filename credits.BANLIST.save(filename) +mgx = re.compile(b'''^(?:@(?P[^ ]+)\ )? + (?::(?P + (?P[a-zA-Z][^!@ ]*) + (?: !(?P[^@ ]+))? + (?:@(?P[^ ]+))? + )\ )? + (?P(?:[a-zA-Z]+|[0-9]{3})) + (?P(?:\ [^:][^ ]*)*)(?:\ :(?P.*))? + $''', re.X) class Message: - def __init__ (self, line, timestamp, private = False): - self.raw = line - self.time = timestamp - self.channel = None - self.content = b'' - self.ctcp = False - line = line.rstrip() #remove trailing 'rn' + def __init__(self, raw_line, timestamp, private = False): + self.raw = raw_line.rstrip() # remove trailing crlf + self.tags = { 'time': timestamp } + self.params = list() - words = line.split(b' ') - if words[0][0] == 58: #58 is : in ASCII table - self.sender = words[0][1:].decode() - self.cmd = words[1].decode() - else: - self.cmd = words[0].decode() - self.sender = None + p = mgx.match(raw_line.rstrip()) - if self.cmd == 'PING': - self.content = words[1] - elif self.sender is not None: - self.nick = (self.sender.split('!'))[0] - if self.nick != self.sender: - self.realname = (self.sender.split('!'))[1] - else: - self.realname = self.nick - self.sender = self.nick + "!" + self.realname + # Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee + if p.group("tags"): + for tgs in p.group("tags").decode().split(';'): + tag = tgs.split('=') + if len(tag) > 1: + self.add_tag(tag[0], tag[1]) + else: + self.add_tag(tag[0]) - if len(words) > 2: - self.channel = self.pickWords(words[2:]).decode() + # 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")) - 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:]) - elif self.cmd == '353' and len(words) > 3: - for i in range(2, len(words)): - if words[i][0] == 58: - self.content = words[i:] - #Remove the first : - self.content[0] = self.content[0][1:] - self.channel = words[i-1].decode() - break - elif self.cmd == 'NICK': - self.content = self.pickWords(words[2:]) - elif self.cmd == 'MODE': - self.content = words[3:] - elif self.cmd == '332': - self.channel = words[3] - self.content = self.pickWords(words[4:]) - else: - #print (line) - self.content = self.pickWords(words[3:]) - else: - print (line) - if self.cmd == 'PRIVMSG': - self.channel = words[2].decode() - self.content = b' '.join(words[3:]) - self.decode() + # Parse command + self.cmd = p.group("command").decode() + + # 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 if self.cmd == 'PRIVMSG': - self.parse_content() - self.private = private + self.receivers = self.params[0].decode().split(',') - def parse_content(self): - """Parse or reparse the message content""" # If CTCP, remove 0x01 - if self.ctcp: - self.content = self.content[1:len(self.content)-1] + if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01): + self.is_ctcp = True + self.text = self.decode(self.params[1][1:len(self.params[1])-1]) + else: + self.is_ctcp = False + self.text = self.decode(self.params[1]) # Split content by words try: - self.cmds = shlex.split(self.content) + self.cmds = shlex.split(self.text) except ValueError: - self.cmds = self.content.split(' ') + self.cmds = self.text.split(' ') + + elif self.cmd == '353': # RPL_NAMREPLY + self.receivers = [ self.params[0].decode() ] + self.nicks = self.params[1].decode().split(" ") + + elif self.cmd == '332': + self.receivers = [ self.params[0].decode() ] + self.topic = self.params[1].decode().split(" ") - def pickWords(self, words): - """Parse last argument of a line: can be a single word or a sentence starting with :""" - if len(words) > 0 and len(words[0]) > 0: - if words[0][0] == 58: - return b' '.join(words[0:])[1:] - else: - return words[0] else: - return b'' + for i in range(0, len(self.params)-1): + self.params[i] = self.decode(self.params[i]) - def decode(self): + print(self) + + + def add_tag(self, key, value=None): + """Add an IRCv3.2 Message Tags""" + # Treat special tags + if key == "time": + value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S") + + # Store tag + self.tags[key] = value + + + def decode(self, s): """Decode the content string usign a specific encoding""" - if isinstance(self.content, bytes): + if isinstance(s, bytes): try: - self.content = self.content.decode() + s = s.decode() except UnicodeDecodeError: #TODO: use encoding from config file - self.content = self.content.decode('utf-8', 'replace') + s = s.decode('utf-8', 'replace') + return s + + + def __str__(self): + return "Message " + str(self.__dict__) + def authorize_DEPRECATED(self): """Is nemubot listening for the sender on this channel?""" diff --git a/wiki b/wiki new file mode 160000 index 0000000..a336243 --- /dev/null +++ b/wiki @@ -0,0 +1 @@ +Subproject commit a336243d0090af38604adc8be768b1bc8c04cd5e