Repository: sopel-irc/sopel

Issue:
Channel.privileges not being filled in
Every nick in Channel.privileges is being marked as 0 (no privileges).
This needs to be fixed because the docs tell you to use bot.channels over bot.privileges, and some people may become confused by the bot not recognizing anyone as op/voice.

Sopel Version: current master.
Python Version: 3



Discussion:
I figured this was already on the radar, but good to have the issue in case it wasn't.

It's the reason all of my modules that access privileges still use the deprecated array.

Note: At time of issue opening, current master was v6.4.0.

Definitely needs testing and sorting (if it still exists in 6.5.x) before 7.0. Looooong before, ideally.
I just tested it. (Finally, a use for the `ipython` module! lol) The results aren't pretty.

````
In [3]: bot.channels['#dgw'].privileges
Out[3]:
{Identifier('dgw'): 0,
 Identifier('GlaD0S'): 0,
 Identifier('Internets'): 0,
 Identifier('Kaede'): 0,
 Identifier('Sopel'): 0}

In [4]: bot.privileges['#dgw']
Out[4]:
{Identifier('dgw'): 20,
 Identifier('GlaD0S'): 12,
 Identifier('Internets'): 0,
 Identifier('Kaede'): 12,
 Identifier('Sopel'): 0}
````

This needs a test when fixed, too, to make sure it _keeps_ working.

Resolving patch:
diff --git a/sopel/coretasks.py b/sopel/coretasks.py
--- a/sopel/coretasks.py
+++ b/sopel/coretasks.py
@@ -174,6 +174,8 @@ def handle_names(bot, trigger):
 
     # This could probably be made flexible in the future, but I don't think
     # it'd be worth it.
+    # If this ever needs to be updated, remember to change the mode handling in
+    # the WHO-handler functions below, too.
     mapping = {'+': sopel.module.VOICE,
                '%': sopel.module.HALFOP,
                '@': sopel.module.OP,
@@ -228,7 +230,13 @@ def track_modes(bot, trigger):
         else:
             arg = Identifier(arg)
             for mode in modes:
-                priv = bot.privileges[channel].get(arg, 0)
+                priv = bot.channels[channel].privileges.get(arg, 0)
+                # Throw an exception if the two privilege-tracking data
+                # structures get out of sync. That should never happen.
+                # This is a good place to check that bot.channels is doing
+                # what it's supposed to do before ultimately removing the old,
+                # deprecated bot.privileges structure completely.
+                assert priv == bot.privileges[channel].get(arg, 0)
                 value = mapping.get(mode[1])
                 if value is not None:
                     if mode[0] == '+':
@@ -236,6 +244,7 @@ def track_modes(bot, trigger):
                     else:
                         priv = priv & ~value
                     bot.privileges[channel][arg] = priv
+                    bot.channels[channel].privileges[arg] = priv
 
 
 @sopel.module.rule('.*')
@@ -654,10 +663,11 @@ def recv_whox(bot, trigger):
         return LOGGER.warning('While populating `bot.accounts` a WHO response was malformed.')
     _, _, channel, user, host, nick, status, account = trigger.args
     away = 'G' in status
-    _record_who(bot, channel, user, host, nick, account, away)
+    modes = ''.join([c for c in status if c in '~&@%+'])
+    _record_who(bot, channel, user, host, nick, account, away, modes)
 
 
-def _record_who(bot, channel, user, host, nick, account=None, away=None):
+def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=None):
     nick = Identifier(nick)
     channel = Identifier(channel)
     if nick not in bot.users:
@@ -668,9 +678,18 @@ def _record_who(bot, channel, user, host, nick, account=None, away=None):
     else:
         user.account = account
     user.away = away
+    priv = 0
+    if modes:
+        mapping = {'+': sopel.module.VOICE,
+           '%': sopel.module.HALFOP,
+           '@': sopel.module.OP,
+           '&': sopel.module.ADMIN,
+           '~': sopel.module.OWNER}
+        for c in modes:
+            priv = priv | mapping[c]
     if channel not in bot.channels:
         bot.channels[channel] = Channel(channel)
-    bot.channels[channel].add_user(user)
+    bot.channels[channel].add_user(user, privs=priv)
 
 
 @sopel.module.event(events.RPL_WHOREPLY)
@@ -678,8 +697,9 @@ def _record_who(bot, channel, user, host, nick, account=None, away=None):
 @sopel.module.priority('high')
 @sopel.module.unblockable
 def recv_who(bot, trigger):
-    channel, user, host, _, nick, = trigger.args[1:6]
-    _record_who(bot, channel, user, host, nick)
+    channel, user, host, _, nick, status = trigger.args[1:7]
+    modes = ''.join([c for c in status if c in '~&@%+'])
+    _record_who(bot, channel, user, host, nick, modes=modes)
 
 
 @sopel.module.event(events.RPL_ENDOFWHO)
diff --git a/sopel/tools/target.py b/sopel/tools/target.py
--- a/sopel/tools/target.py
+++ b/sopel/tools/target.py
@@ -67,10 +67,10 @@ def clear_user(self, nick):
         if user is not None:
             user.channels.pop(self.name, None)
 
-    def add_user(self, user):
+    def add_user(self, user, privs=0):
         assert isinstance(user, User)
         self.users[user.nick] = user
-        self.privileges[user.nick] = 0
+        self.privileges[user.nick] = privs
         user.channels[self.name] = self
 
     def rename_user(self, old, new):


Test patch: