Skip to content

Commit

Permalink
Add tracking of users and their accounts
Browse files Browse the repository at this point in the history
This uses some of the code that @maxpowa wrote for #941, but gives a
somewhat more intuitive API. It also paves the way for potentially
adding direct support for away-notify and metadata-notify.
  • Loading branch information
embolalia committed Dec 6, 2015
1 parent 664d9f4 commit ea7f144
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 21 deletions.
3 changes: 3 additions & 0 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ def __init__(self, config, daemon=False):
bitwise integer value, determined by combining the appropriate constants
from `module`."""

self.channels_ = dict() # name to chan obj
self.users = dict() # name to user obj

self.db = SopelDB(config)
"""The bot's database."""

Expand Down
160 changes: 139 additions & 21 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
from __future__ import unicode_literals, absolute_import, print_function, division


from random import randint
import re
import sys
import time
import sopel
import sopel.module
from sopel.tools import Identifier, iteritems
from sopel.tools.target import User, Channel
import base64
from sopel.logger import get_logger

Expand All @@ -31,6 +33,7 @@
LOGGER = get_logger(__name__)

batched_caps = {}
who_reqs = {} # Keeps track of reqs coming from this module, rather than others


def auth_after_register(bot):
Expand Down Expand Up @@ -234,21 +237,21 @@ def track_nicks(bot, trigger):
value = bot.privileges[channel].pop(old)
bot.privileges[channel][new] = value

for channel in bot.channels_.values():
channel.rename_user(old, new)
if old in bot.users:
bot.users[new] = bot.users.pop(old)


@sopel.module.rule('(.*)')
@sopel.module.event('PART')
@sopel.module.priority('high')
@sopel.module.thread(False)
@sopel.module.unblockable
def track_part(bot, trigger):
if trigger.nick == bot.nick:
bot.channels.remove(trigger.sender)
del bot.privileges[trigger.sender]
else:
try:
del bot.privileges[trigger.sender][trigger.nick]
except KeyError:
pass
nick = trigger.nick
channel = trigger.sender
_remove_from_channel(bot, nick, channel)


@sopel.module.rule('.*')
Expand All @@ -258,17 +261,58 @@ def track_part(bot, trigger):
@sopel.module.unblockable
def track_kick(bot, trigger):
nick = Identifier(trigger.args[1])
channel = trigger.sender
_remove_from_channel(bot, nick, channel)


def _remove_from_channel(bot, nick, channel):
if nick == bot.nick:
bot.channels.remove(trigger.sender)
del bot.privileges[trigger.sender]
del bot.privileges[channel]

This comment has been minimized.

Copy link
@elad661

elad661 Dec 6, 2015

Contributor

probably best to make sure channel is in bot.privileges before deleting it. Also, since we are way too multi threaded, it might make sense to add locks around privileges and .channels_

bot.channels.remove(channel)

if channel in bot.channels_:
del bot.channels_[channel]

lost_users = []
for nick_, user in bot.users.items():
if channel in user.channels:
del user.channels[channel]
if not user.channels:
lost_users.append(nick_)
for nick_ in lost_users:
del bot.users[nick_]
else:
if nick in bot.privileges[channel]:
del bot.privileges[channel][nick]

user = bot.users.get(nick)
if user and channel in user.channels:
bot.channels_[channel].clear_user(nick)
if not user.channels:
del bot.users[nick]
bot.channels_[channel]


def _accounts_enabled(bot):
return ('account-notify' in bot.enabled_capabilities and
'extended-join' in bot.enabled_capabilities)


def _send_who(bot, channel):
if _accounts_enabled(bot):
# WHOX syntax, see http://faerion.sourceforge.net/doc/irc/whox.var
# Needed for accounts in who replies. The random integer is a param
# to identify the reply as one from this command, because if someone
# else sent it, we have no fucking way to know what the format is.
rand = str(randint(0, 999))
while rand in who_reqs:
rand = str(randint(0, 999))
who_reqs[rand] = channel
bot.write(['WHO', channel, 'a%nuacht,' + rand])
else:
# Temporary fix to stop KeyErrors from being sent to channel
# The privileges dict may not have all nicks stored at all times
# causing KeyErrors
try:
del bot.privileges[trigger.sender][nick]
except KeyError:
pass
# We might be on an old network, but we still care about keeping our
# user list updated
bot.write(['WHO', channel])


@sopel.module.rule('.*')
Expand All @@ -280,8 +324,19 @@ def track_join(bot, trigger):
if trigger.nick == bot.nick and trigger.sender not in bot.channels:
bot.channels.append(trigger.sender)
bot.privileges[trigger.sender] = dict()
if trigger.nick == bot.nick and trigger.sender not in bot.channels_:

This comment has been minimized.

Copy link
@elad661

elad661 Dec 6, 2015

Contributor

I think nesting these two under if trigger.nick == bot.nick would be more readable and also will be slightly more efficient in a probably not noticeable way

bot.channels_[trigger.sender] = Channel(trigger.sender)
_send_who(bot, trigger.sender)
bot.privileges[trigger.sender][trigger.nick] = 0

user = bot.users.get(trigger.nick)
if user is None:
user = User(trigger.nick, trigger.user, trigger.host)
bot.channels_[trigger.sender].add_user(user)

if len(trigger.args) > 1 and trigger.args[1] != '*' and _accounts_enabled(bot):
user.account = trigger.args[1]


@sopel.module.rule('.*')
@sopel.module.event('QUIT')
Expand All @@ -292,6 +347,9 @@ def track_quit(bot, trigger):
for chanprivs in bot.privileges.values():
if trigger.nick in chanprivs:
del chanprivs[trigger.nick]
for channel in bot.channels_.values():
channel.clear_user(trigger.nick)
bot.users.pop(trigger.nick, None)


@sopel.module.rule('.*')
Expand All @@ -314,10 +372,11 @@ def recieve_cap_list(bot, trigger):
if req[0] and req[2]:
# Call it.
req[2](bot, req[0] + trigger)
# Server is acknowledinge SASL for us.
elif (trigger.args[0] == bot.nick and trigger.args[1] == 'ACK' and
'sasl' in trigger.args[2]):
recieve_cap_ack_sasl(bot)
# Server is acknowledging SASL for us.
elif trigger.args[1] == 'ACK':
if (trigger.args[0] == bot.nick and 'sasl' in trigger.args[2]):
recieve_cap_ack_sasl(bot)
bot.enabled_capabilities.add(trigger.args[2].strip())


def recieve_cap_ls_reply(bot, trigger):
Expand Down Expand Up @@ -347,6 +406,15 @@ def recieve_cap_ls_reply(bot, trigger):
# parse it, so we don't need to worry if it fails.
bot._cap_reqs['multi-prefix'] = (['', 'coretasks', None, None],)

def acct_warn(bot, cap):
LOGGER.info('Server does not support {}, or it conflicts with a custom '
'module. User account validation is not available.'.format(
cap))
if 'account-notify' not in bot._cap_reqs:
bot._cap_reqs['account-notify'] = (['', 'coretasks', None, acct_warn],)
if 'extended-join' not in bot._cap_reqs:
bot._cap_reqs['extended-join'] = (['', 'coretasks', None, acct_warn],)

for cap, reqs in iteritems(bot._cap_reqs):
# At this point, we know mandatory and prohibited don't co-exist, but
# we need to call back for optionals if they're also prohibited
Expand Down Expand Up @@ -493,3 +561,53 @@ def blocks(bot, trigger):
return
else:
bot.reply(STRINGS['huh'])


@sopel.module.event('ACCOUNT')
@sopel.module.rule('.*')
def account_notify(bot, trigger):
print(trigger.nick)
if trigger.nick not in bot.users:
bot.users[trigger.nick] = User(trigger.nick, trigger.user, trigger.host)
account = trigger.args[0]
if account == '*':
account = None
bot.users[trigger.nick].account = account


@sopel.module.event('354')
@sopel.module.rule('.*')
@sopel.module.priority('high')
@sopel.module.unblockable
def recv_whox(bot, trigger):
if (len(trigger.args) < 2 or trigger.args[1] not in who_reqs or
not _accounts_enabled(bot)):
# Ignored, some module probably called WHO
# TODO a separate 352 handler function
return
if len(trigger.args) != 7:
return LOGGER.warning('While populating `bot.accounts` a WHO response was malformed.')
print(trigger.args)
_, _, channel, user, host, nick, account = trigger.args
nick = Identifier(nick)
channel = Identifier(channel)
if nick not in bot.users:
bot.users[nick] = User(nick, user, host)
user = bot.users[nick]
if account == '0':
user.account = None
else:
user.account = account
if channel not in bot.channels_:
bot.channels_[channel] = Channel(channel)
bot.channels_[channel].add_user(user)

This comment has been minimized.

Copy link
@elad661

elad661 Dec 6, 2015

Contributor

Did you forget to pop who_reqs?

This comment has been minimized.

Copy link
@elad661

elad661 Dec 6, 2015

Contributor

nevermind, I can see you do it in end_who. This code is kinda confusing but its the protocol's fault


@sopel.module.event('315')
@sopel.module.rule('.*')
@sopel.module.priority('high')
@sopel.module.unblockable
def end_who(bot, trigger):
if not _accounts_enabled(bot):
return

This comment has been minimized.

Copy link
@elad661

elad661 Dec 6, 2015

Contributor

wouldn't it be better to change it to

if _accounts_enabled(bot):
    who_reqs.pop(trigger.args[1], None)

?

who_reqs.pop(trigger.args[1], None)
64 changes: 64 additions & 0 deletions sopel/tools/target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division

import functools
from sopel.tools import Identifier


@functools.total_ordering
class User(object):
def __init__(self, nick, user, host):
assert isinstance(nick, Identifier)
self.nick = nick
self.user = user
self.host = host
self.channels = {}

hostmask = property(lambda self: '{}!{}@{}'.format(self.nick, self.user,
self.host))

def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.name == other.name

def __lt__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.name < other.name


@functools.total_ordering
class Channel(object):
def __init__(self, name):
assert isinstance(name, Identifier)
self.name = name
self.users = {}
self.privileges = {}

def clear_user(self, nick):
user = self.users[nick]
user.channels.pop(self.name, None)
del self.users[nick]
del self.privileges[nick]

def add_user(self, user):
assert isinstance(user, User)
self.users[user.nick] = user
self.privileges[user.nick] = 0

def rename_user(self, old, new):
if old in self.users:
self.users[new] = self.users.pop(old)
if old in self.privileges:
self.privileges[new] = self.privileges.pop(old)

def __eq__(self, other):
if not isinstance(other, Channel):
return NotImplemented
return self.name == other.name

def __lt__(self, other):
if not isinstance(other, Channel):
return NotImplemented
return self.name < other.name

0 comments on commit ea7f144

Please sign in to comment.