Add IrcState.nicksToAccount

To keep track of network accounts using various IRCv3 specs
This commit is contained in:
Valentin Lorentz 2022-11-20 20:27:36 +01:00
parent 97d67777d6
commit eaf0ce0f59
2 changed files with 154 additions and 2 deletions

View File

@ -680,6 +680,12 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
Stores the last hostmask of a seen nick.
:type: ircutils.IrcDict[str, str]
.. attribute:: nicksToAccounts
Stores the current network account name of a seen nick.
:type: ircutils.IrcDict[str, str]
"""
__firewalled__ = {'addMsg': None}
@ -689,7 +695,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
nicksToHostmasks=None, channels=None,
capabilities_req=None,
capabilities_ack=None, capabilities_nak=None,
capabilities_ls=None):
capabilities_ls=None,
nicksToAccounts=None):
self.fsm = IrcStateFsm()
if history is None:
history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength())
@ -697,6 +704,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
supported = utils.InsensitivePreservingDict()
if nicksToHostmasks is None:
nicksToHostmasks = ircutils.IrcDict()
if nicksToAccounts is None:
nicksToAccounts = ircutils.IrcDict()
if channels is None:
channels = ircutils.IrcDict()
self.capabilities_req = capabilities_req or set()
@ -708,6 +717,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.history = history
self.channels = channels
self.nicksToHostmasks = nicksToHostmasks
self.nicksToAccounts = nicksToAccounts
# Batches usually finish and are way shorter than 3600s, but
# we need to:
@ -725,6 +735,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.channels.clear()
self.supported.clear()
self.nicksToHostmasks.clear()
self.nicksToAccounts.clear()
self.capabilities_req = set()
self.capabilities_ack = set()
self.capabilities_nak = set()
@ -745,13 +756,16 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
def __reduce__(self):
return (self.__class__, (self.history, self.supported,
self.nicksToHostmasks, self.channels))
self.nicksToHostmasks,
self.nicksToAccounts,
self.channels))
def __eq__(self, other):
return self.history == other.history and \
self.channels == other.channels and \
self.supported == other.supported and \
self.nicksToHostmasks == other.nicksToHostmasks and \
self.nicksToAccounts == other.nicksToAccounts and \
self.batches == other.batches
def __ne__(self, other):
@ -761,6 +775,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
ret = self.__class__()
ret.history = copy.deepcopy(self.history)
ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks)
ret.nicksToAccounts = copy.deepcopy(self.nicksToAccounts)
ret.channels = copy.deepcopy(self.channels)
ret.batches = copy.deepcopy(self.batches)
return ret
@ -770,6 +785,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.history.append(msg)
if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK':
self.nicksToHostmasks[msg.nick] = msg.prefix
if 'account' in msg.server_tags:
self.nicksToAccounts[msg.nick] = msg.server_tags['account']
if 'batch' in msg.server_tags:
batch_name = msg.server_tags['batch']
assert batch_name in self.batches, \
@ -788,6 +805,11 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
"""Returns the hostmask for a given nick."""
return self.nicksToHostmasks[nick]
def nickToAccount(self, nick):
"""Returns the account for a given nick, or None if the nick is logged
out."""
return self.nicksToAccounts[nick]
def getParentBatches(self, msg):
"""Given an IrcMsg, returns a list of all batches that contain it,
innermost first.
@ -957,6 +979,11 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
(n, t, user, ip, host, nick, status, account, gecos) = msg.args
hostmask = '%s!%s@%s' % (nick, user, host)
self.nicksToHostmasks[nick] = hostmask
if account == '0':
# logged out
self.nicksToAccounts[nick] = None
else:
self.nicksToAccounts[nick] = account
def do353(self, irc, msg):
# NAMES reply.
@ -978,6 +1005,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
stripped_item = item.lstrip(prefix_chars)
item_prefix = item[0:-len(stripped_item)]
if ircutils.isUserHostmask(stripped_item):
# https://ircv3.net/specs/extensions/userhost-in-names
nick = ircutils.nickFromHostmask(stripped_item)
self.nicksToHostmasks[nick] = stripped_item
name = item_prefix + nick
@ -989,11 +1017,20 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
c.modes['s'] = None
def doChghost(self, irc, msg):
# https://ircv3.net/specs/extensions/chghost
(user, host) = msg.args
nick = msg.nick
hostmask = '%s!%s@%s' % (nick, user, host)
self.nicksToHostmasks[nick] = hostmask
def doAccount(self, irc, msg):
# https://ircv3.net/specs/extensions/account-notify
account = msg.args[0]
if account == '*':
self.nicksToAccounts[msg.nick] = None
else:
self.nicksToAccounts[msg.nick] = account
def doJoin(self, irc, msg):
for channel in msg.args[0].split(','):
if channel in self.channels:
@ -1004,6 +1041,12 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
self.channels[channel] = chan
# I don't know why this assert was here.
#assert msg.nick == irc.nick, msg
if 'extended-join' in self.capabilities_ack:
account = msg.args[1]
if account == '*':
self.nicksToAccounts[msg.nick] = None
else:
self.nicksToAccounts[msg.nick] = account
def do367(self, irc, msg):
# Example:
@ -1083,6 +1126,8 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
if msg.nick in self.nicksToHostmasks:
# If we're quitting, it may not be.
del self.nicksToHostmasks[msg.nick]
if msg.nick in self.nicksToAccounts:
del self.nicksToAccounts[msg.nick]
def doTopic(self, irc, msg):
if len(msg.args) == 1:
@ -1100,6 +1145,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
def doNick(self, irc, msg):
newNick = msg.args[0]
oldNick = msg.nick
try:
if msg.user and msg.host:
# Nick messages being handed out from the bot itself won't
@ -1109,6 +1155,13 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
del self.nicksToHostmasks[oldNick]
except KeyError:
pass
try:
self.nicksToAccounts[newNick] = self.nicksToAccounts[oldNick]
del self.nicksToAccounts[oldNick]
except KeyError:
pass
channel_names = ircutils.IrcSet()
for (name, channel) in self.channels.items():
if msg.nick in channel.users:

View File

@ -579,6 +579,105 @@ class IrcStateTestCase(SupyTestCase):
command='CHGHOST', args=['bar2', 'baz2']))
self.assertEqual(st.nickToHostmask('foo'), 'foo!bar2@baz2')
def testNickToAccountBaseJoin(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz'))
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='bar!baz@qux'))
with self.assertRaises(KeyError):
st.nickToAccount('foo')
st.nickToAccount('bar')
def testNickToAccountExtendedJoin(self):
st = irclib.IrcState()
st.capabilities_ack.add('extended-join')
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='foo!bar@baz',
args=['#foo', 'account1', 'real name1']))
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='bar!baz@qux',
args=['#foo', 'account2', 'real name2']))
st.addMsg(self.irc, ircmsgs.IrcMsg(
command='JOIN', prefix='baz!qux@quux',
args=['#foo', '*', 'real name3']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
self.assertIsNone(st.nickToAccount('baz'))
with self.assertRaises(KeyError):
st.nickToAccount('qux')
# QUIT erases the entry
with self.subTest("QUIT"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz'))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('bar'), 'account2')
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
# NICK moves the entry
with self.subTest("NICK"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='NICK', args=['foo2']))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('foo2'), 'account1')
self.assertEqual(st2.nickToAccount('bar'), 'account2')
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
# NICK moves the entry (and overwrites if needed)
with self.subTest("NICK with overwrite"):
st2 = st.copy()
st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='NICK', args=['bar']))
with self.assertRaises(KeyError):
st2.nickToAccount('foo')
self.assertEqual(st2.nickToAccount('bar'), 'account1')
self.assertEqual(st.nickToAccount('foo'), 'account1')
self.assertEqual(st.nickToAccount('bar'), 'account2')
def testNickToAccountWho(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='352', # RPL_WHOREPLY
args=[self.irc.nick, '#chan', 'bar', 'baz', 'server.example',
'foo', 'H', '0 real name']))
with self.assertRaises(KeyError):
st.nickToAccount('foo')
def testNickToAccountWhox(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL
args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz',
'foo', 'H', 'account1', 'real name']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL
args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz',
'foo', 'H', '0', 'real name']))
self.assertIsNone(st.nickToAccount('foo'))
def testAccountNotify(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['account1']))
self.assertEqual(st.nickToAccount('foo'), 'account1')
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['account2']))
self.assertEqual(st.nickToAccount('foo'), 'account2')
st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz',
command='ACCOUNT', args=['*']))
self.assertIsNone(st.nickToAccount('foo'))
def testEq(self):
state1 = irclib.IrcState()
state2 = irclib.IrcState()