diff --git a/plugins/NickAuth/plugin.py b/plugins/NickAuth/plugin.py index c84d56b35..b8116dbc6 100644 --- a/plugins/NickAuth/plugin.py +++ b/plugins/NickAuth/plugin.py @@ -147,6 +147,13 @@ class NickAuth(callbacks.Plugin): irc.queueMsg(ircmsgs.whois(nick, nick)) auth = wrap(auth, []) + def inFilter(self, irc, msg): + """If the messages has a server tag with account name, tries to + authenticate it.""" + if msg.server_tags and 'account' in msg.server_tags: + self._auth(irc, msg.prefix, msg.server_tags['account']) + return msg + def do330(self, irc, msg): mynick, theirnick, theiraccount, garbage = msg.args # I would like to use a dict comprehension, but we have to support @@ -176,18 +183,8 @@ class NickAuth(callbacks.Plugin): account = msg.args[0] user = ircdb.users.getUserFromNick(irc.network, account) - if not user: - try: - user = ircdb.users.getUser(msg.prefix) - except KeyError: - user = None - - if user: - if account == '*': - user.clearAuth() - else: - user.addAuth(msg.prefix) - ircdb.users.setUser(user, flush=False) + if account != '*': + self._auth(irc, msg.prefix, account) def doJoin(self, irc, msg): @@ -196,18 +193,8 @@ class NickAuth(callbacks.Plugin): return account = msg.args[1] - user = ircdb.users.getUserFromNick(irc.network, account) - - if not user: - try: - user = ircdb.users.getUser(msg.prefix) - except KeyError: - user = None - - if user: - if account != '*': - user.addAuth(msg.prefix) - ircdb.users.setUser(user, flush=False) + if account != '*': + self._auth(irc, msg.prefix, account) def do354(self, irc, msg): if len(msg.args) != 6 or msg.args[1] != '1': @@ -217,6 +204,12 @@ class NickAuth(callbacks.Plugin): prefix = '%s!%s@%s' % (nick, ident, host) user = ircdb.users.getUserFromNick(irc.network, account) + if account != '0': + self._auth(irc, prefix, account) + + def _auth(self, irc, prefix, account): + user = ircdb.users.getUserFromNick(irc.network, account) + if not user: try: user = ircdb.users.getUser(prefix) @@ -224,7 +217,6 @@ class NickAuth(callbacks.Plugin): user = None if user: - if account != '0': user.addAuth(prefix) ircdb.users.setUser(user, flush=False) diff --git a/plugins/NickCapture/plugin.py b/plugins/NickCapture/plugin.py index 17b312204..974d50657 100644 --- a/plugins/NickCapture/plugin.py +++ b/plugins/NickCapture/plugin.py @@ -45,6 +45,12 @@ class NickCapture(callbacks.Plugin): self.__parent = super(NickCapture, self) self.__parent.__init__(irc) self.lastIson = 0 + self.monitoring = [] + + def die(self): + for irc in self.monitoring: + nick = self._getNick(irc.network) + irc.unmonitor(nick) def _getNick(self, network): network_nick = conf.supybot.networks.get(network).nick() @@ -60,10 +66,16 @@ class NickCapture(callbacks.Plugin): # We used to check this, but nicksToHostmasks is never cleared # except on reconnects, which can cause trouble. # if nick not in irc.state.nicksToHostmasks: - self._ison(irc, nick) + if 'monitor' in irc.state.supported: + if irc not in self.monitoring: + irc.monitor(nick) + self.monitoring.append(irc) + else: + self._ison(irc, nick) self.__parent.__call__(irc, msg) def _ison(self, irc, nick): + assert 'monitor' not in irc.state.supported if self.registryValue('ison'): now = time.time() if now - self.lastIson > self.registryValue('ison.period'): @@ -95,6 +107,16 @@ class NickCapture(callbacks.Plugin): nick = self._getNick(irc.network) if nick: self._sendNick(irc, nick) + + def do731(self, irc, msg): + """This is sent by the MONITOR when a nick goes offline.""" + nick = self._getNick(irc.network) + for target in msg.args[1].split(','): + if nick == target: + self._sendNick(irc, nick) + self.monitoring.remove(irc) + irc.unmonitor(nick) + break NickCapture = internationalizeDocstring(NickCapture) Class = NickCapture diff --git a/src/irclib.py b/src/irclib.py index 94380f9b2..ee122084c 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -666,6 +666,7 @@ class Irc(IrcCommandDispatcher): self._setNonResettingVariables() self._queueConnectMessages() self.startedSync = ircutils.IrcDict() + self.monitoring = ircutils.IrcDict() def isChannel(self, s): """Helper function to check whether a given string is a channel on @@ -965,7 +966,8 @@ class Irc(IrcCommandDispatcher): elif self.sasl_username and self.sasl_password: self.sasl = 'plain' - for cap in ('account-notify', 'extended-join', 'multi-prefix'): + for cap in ('account-notify', 'extended-join', 'multi-prefix', + 'metadata-notify', 'account-tag'): self.queueMsg(ircmsgs.IrcMsg(command='CAP', args=('REQ', cap))) if self.sasl: @@ -1041,6 +1043,38 @@ class Irc(IrcCommandDispatcher): command='CAP', args=('END',))) + def monitor(self, targets): + """Increment a counter of how many callbacks monitor each target; + and send a MONITOR + to the server if the target is not yet + monitored.""" + if isinstance(targets, str): + targets = [targets] + not_yet_monitored = set() + for target in targets: + if target in self.monitoring: + self.monitoring[target] += 1 + else: + not_yet_monitored.add(target) + self.monitoring[target] = 1 + if not_yet_monitored: + self.queueMsg(ircmsgs.monitor('+', not_yet_monitored)) + return not_yet_monitored + + def unmonitor(self, targets): + """Decrements a counter of how many callbacks monitor each target; + and send a MONITOR - to the server if the counter drops to 0.""" + if isinstance(targets, str): + targets = [targets] + should_be_unmonitored = set() + for target in targets: + self.monitoring[target] -= 1 + if self.monitoring[target] == 0: + del self.monitoring[target] + should_be_unmonitored.add(target) + if should_be_unmonitored: + self.queueMsg(ircmsgs.monitor('-', should_be_unmonitored)) + return should_be_unmonitored + def do903(self, msg): log.info('%s: SASL authentication successful', self.network) self.queueMsg(ircmsgs.IrcMsg(command='CAP', args=('END',))) diff --git a/src/ircmsgs.py b/src/ircmsgs.py index 54ab272b3..45bf3104b 100644 --- a/src/ircmsgs.py +++ b/src/ircmsgs.py @@ -50,6 +50,37 @@ from .utils.iter import all class MalformedIrcMsg(ValueError): pass +# http://ircv3.net/specs/core/message-tags-3.2.html#escaping-values +SERVER_TAG_ESCAPE = [ + ('\\', '\\\\'), # \ -> \\ + (' ', r'\s'), + (';', r'\:'), + ('\r', r'\r'), + ('\n', r'\n'), + ] +escape_server_tag_value = utils.str.MultipleReplacer( + dict(SERVER_TAG_ESCAPE)) +unescape_server_tag_value = utils.str.MultipleReplacer( + dict(map(lambda x:(x[1],x[0]), SERVER_TAG_ESCAPE))) + +def parse_server_tags(s): + server_tags = {} + for tag in s.split(';'): + if '=' not in tag: + server_tags[tag] = None + else: + (key, value) = tag.split('=', 1) + server_tags[key] = unescape_server_tag_value(value) + return server_tags +def format_server_tags(server_tags): + parts = [] + for (key, value) in server_tags.items(): + if value is None: + parts.append(key) + else: + parts.append('%s=%s' % (key, escape_server_tag_value(value))) + return '@' + ';'.join(parts) + class IrcMsg(object): """Class to represent an IRC message. @@ -81,7 +112,8 @@ class IrcMsg(object): # data. Goodbye, __slots__. # On second thought, let's use methods for tagging. __slots__ = ('args', 'command', 'host', 'nick', 'prefix', 'user', - '_hash', '_str', '_repr', '_len', 'tags', 'reply_env') + '_hash', '_str', '_repr', '_len', 'tags', 'reply_env', + 'server_tags') def __init__(self, s='', command='', args=(), prefix='', msg=None, reply_env=None): assert not (msg and s), 'IrcMsg.__init__ cannot accept both s and msg' @@ -99,6 +131,11 @@ class IrcMsg(object): if not s.endswith('\n'): s += '\n' self._str = s + if s[0] == '@': + (server_tags, s) = s.split(' ', 1) + self.server_tags = parse_server_tags(server_tags[1:]) + else: + self.server_tags = {} if s[0] == ':': self.prefix, s = s[1:].split(None, 1) else: @@ -818,6 +855,19 @@ def ison(nick, prefix='', msg=None): prefix = msg.prefix return IrcMsg(prefix=prefix, command='ISON', args=(nick,), msg=msg) +def monitor(subcommand, nicks=None, prefix='', msg=None): + if conf.supybot.protocols.irc.strictRfc(): + assert isNick(nick), repr(nick) + assert subcommand in '+-CLS' + if subcommand in 'CLS': + assert nicks is None + if msg and not prefix: + prefix = msg.prefix + if not isinstance(nicks, str): + nicks = ','.join(nicks) + return IrcMsg(prefix=prefix, command='MONITOR', args=(subcommand, nicks), + msg=msg) + def error(s, msg=None): return IrcMsg(command='ERROR', args=(s,), msg=msg) diff --git a/test/test_irclib.py b/test/test_irclib.py index bb85f33fa..034ec725c 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -391,6 +391,10 @@ class IrcTestCase(SupyTestCase): m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) m = self.irc.takeMsg() + self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) + m = self.irc.takeMsg() + self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) + m = self.irc.takeMsg() self.failUnless(m.command == 'USER', 'Expected USER, got %r.' % m) def testPingResponse(self): @@ -492,6 +496,8 @@ class IrcCallbackTestCase(SupyTestCase): ircmsgs.IrcMsg(command='CAP', args=('REQ', 'account-notify')), ircmsgs.IrcMsg(command='CAP', args=('REQ', 'extended-join')), ircmsgs.IrcMsg(command='CAP', args=('REQ', 'multi-prefix')), + ircmsgs.IrcMsg(command='CAP', args=('REQ', 'metadata-notify')), + ircmsgs.IrcMsg(command='CAP', args=('REQ', 'account-tag')), ircmsgs.IrcMsg(command='CAP', args=('END',)), ircmsgs.user('limnoria', user) ] diff --git a/test/test_ircmsgs.py b/test/test_ircmsgs.py index 24960f500..583d0896a 100644 --- a/test/test_ircmsgs.py +++ b/test/test_ircmsgs.py @@ -128,6 +128,19 @@ class IrcMsgTestCase(SupyTestCase): m.tag('repliedTo', 12) self.assertEqual(m.repliedTo, 12) + def testServerTags(self): + s = '@aaa=b\\:bb;ccc;example.com/ddd=ee\\\\se ' \ + ':nick!ident@host.com PRIVMSG me :Hello' + m = ircmsgs.IrcMsg(s) + self.assertEqual(m.server_tags, { + 'aaa': 'b;bb', + 'ccc': None, + 'example.com/ddd': 'ee\\se'}) + self.assertEqual(m.prefix, 'nick!ident@host.com') + self.assertEqual(m.command, 'PRIVMSG') + self.assertEqual(m.args, ('me', 'Hello')) + self.assertEqual(str(m), s + '\n') + class FunctionsTestCase(SupyTestCase): def testIsAction(self): L = [':jemfinch!~jfincher@ts26-2.homenet.ohio-state.edu PRIVMSG'