Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MODE between WHO and RPL_WHOREPLY is lost #2562

Open
half-duplex opened this issue Nov 13, 2023 · 4 comments
Open

MODE between WHO and RPL_WHOREPLY is lost #2562

half-duplex opened this issue Nov 13, 2023 · 4 comments
Labels
Bug Things to squish; generally used for issues Needs Triage Issues that need to be reviewed and categorized

Comments

@half-duplex
Copy link
Member

half-duplex commented Nov 13, 2023

Description

I recently encountered an issue where a channel op (+o) was not being treated as an op by the bot. Checking the raw log, I found this when they last joined the channel:

<<      ':[email protected] JOIN :#channel\r\n'
>>      'WHO User\r\n'
<<      ':ChanServ!ChanServ@services MODE #channel +o User\r\n'
<<      ':server 352 Sopel #channel user user.host foo.server User Hr :0 John Smith\r\n'
<<      ':server 315 Sopel User :End of /WHO list.\r\n'

I'm not really sure how to handle this correctly, or if it's even Sopel's responsibility.

Reproduction steps

Run this test:

def test_bot_who_mode_order(mockbot, ircfactory):
    irc = ircfactory(mockbot)
    irc.bot._isupport = isupport.ISupport(chanmodes=("o", "", "", "", tuple()))
    irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES
    irc.channel_joined("#test")
    mockbot.on_message(":[email protected] JOIN :#test")
    assert mockbot.backend.message_sent == rawlist("WHO Alex")
    mockbot.on_message(":[email protected] MODE #test +o Alex")
    mockbot.on_message(":server.local 352 Sopel #test alex alex.local specific.server.local Alex Hr :0 Alex")
    mockbot.on_message(":server.local 315 Sopel Alex :End of /WHO list.")
    assert mockbot.channels["#test"].privileges[Identifier("Alex")] == OP
test/test_coretasks.py:129: in test_bot_who_mode_order
    assert mockbot.channels["#test"].privileges[Identifier("Alex")] == OP
E   assert 0 == <AccessLevel.OP: 4>

Expected behavior

bot.channels["#channel"].privileges["User"] = OP

Sopel version

Roughly 51300a1

Installation method

pip install

IRCd

Unreal3.2.10.6

@half-duplex half-duplex added Bug Things to squish; generally used for issues Needs Triage Issues that need to be reviewed and categorized labels Nov 13, 2023
@half-duplex
Copy link
Member Author

I wonder if modern Unreal has the same interleaving behavior, considering 3.2 was released in 2004 and EOL in 2016...
https://www.unrealircd.org/docs/UnrealIRCd_releases

@dgw
Copy link
Member

dgw commented Nov 15, 2023

Hmm, this smells race-y, but on the server side. I don't think a client should have to be in the business of deciding which messages from the server are trustworthy.

It's unfortunate in a way that Unreal 3.2.10 added support for away-notify, as Sopel's periodic polling of WHO information would certainly fix the permissions-state desync no more than max(120, len(bot.channels) * 30) seconds after the problematic WHO request if it weren't disabled by the availability of away-notify:

sopel/sopel/coretasks.py

Lines 916 to 925 in 51300a1

@plugin.interval(30)
def _periodic_send_who(bot):
"""Periodically send a WHO request to keep user information up-to-date."""
if bot.capabilities.is_enabled('away-notify'):
# WHO not needed to update 'away' status
return
# Loop through the channels to find the one that has the longest time since the last WHO
# request, and issue a WHO request only if the last request for the channel was more than
# 120 seconds ago.

I'm playing with some possible resolutions locally. The trick will be figuring out when to selectively ignore what the WHO response says about privileges.

@dgw
Copy link
Member

dgw commented Nov 15, 2023

I'm feeling pretty secure in at minimum leaving this until after 8.0.0 based on testing elsewhere (non-Unreal-3.2 IRCds). tfw is an alt I used for testing; GlaD0S is network services:

2023-11-15 14:29:42,523 <<      ':[email protected] JOIN :#dgw\r\n'
2023-11-15 14:29:42,525 >>      'WHO tfw\r\n'
2023-11-15 14:29:42,525 <<      ':[email protected] MODE #dgw +qo tfw tfw\r\n'
2023-11-15 14:29:42,556 <<      ':irc.rizon.io 352 SopelTest #dgw ~qwebirc my.hostname * tfw Hr~@ :0 Rizon Web IRC\r\n'
2023-11-15 14:29:42,557 <<      ':irc.rizon.io 315 SopelTest tfw :End of /WHO list.\r\n'
2023-11-15 14:29:48,202 <<      ':[email protected] PRIVMSG #dgw :,isop tfw\r\n'
2023-11-15 14:29:48,203 >>      'PRIVMSG #dgw :dgw: tfw is a chanop.\r\n'

This network CAP ACKed both multi-prefix and userhost-in-names, though only multi-prefix is relevant to this scenario. (It gives Sopel the ~@ in status flags instead of just ~.)

A quick testing plugin (isop.py) that I wrote to avoid dealing with ipython gave me those last two lines:

isop.py code
from __future__ import annotations

from sopel import plugin


@plugin.commands('isop')
def isop(bot, trigger):
    if not trigger.group(3):
        bot.reply("You dum-dum. I need a nick!")
        return

    is_not = 'is not'
    if bot.channels[trigger.sender].privileges[trigger.group(3)] >= plugin.OP:
        is_not = 'is'
    bot.reply("{} {} a chanop.".format(trigger.group(3), is_not))

We might need to consider allowing the periodic WHO polling to operate if multi-prefix isn't available too, even on servers supporting away-notify, because otherwise a nick with @+ will appear to Sopel as only @ in NAMES and WHO replies, and a MODE -o nick will leave Sopel thinking nick has no privileges when in fact it still has +v.

@dgw
Copy link
Member

dgw commented Nov 16, 2023

To follow up on the discussion here while bringing it back to the actual bug report: Maybe coretasks could keep track of pending WHO requests, but the User object shouldn't.

One possible solution: queue MODE commands for later processing if a target exists in the pending WHO queue.

Another, perhaps better, solution: Stop updating privileges from RPL_WHOREPLY / RPL_WHOSPCRPL. For both WHO and WHOX's c field, the channel included in the response when querying against a nickname is both arbitrary and optional (can be *), plus the inclusion of status flags for permissions on that channel is also optional.

For users already in the channel when Sopel joins, the initial RPL_NAMREPLY burst will give their current (or at least, highest) permissions. For a user joining after Sopel, the server has to send MODEs anyway.


I think Sopel pulls privileges from WHO responses as a fallback for servers not supporting multi-prefix.

The theory is that if somenick on the channel before Sopel joins has multiple privileges like +ao, but the server only sends the highest status character & (for +a) in NAMES/WHO replies, Sopel will only store the ADMIN privilege flag for somenick. If MODE -a somenick comes in, Sopel will think somenick's privileges are 0 (regular user) until the next WHO of that channel, because it's never seen the @ status prefix or a MODE +o somenick.

multi-prefix makes that situation easier, eliminating the period where Sopel thinks that somenick's privileges are 0, but not all servers support the extension. Plus if a non-multi-prefix server happens to support away-notify, even the fallback breaks: Sopel disables WHO polling with away-notify active.


Written, rewritten, and then deleted to focus on the bug: Ideas about reducing how often Sopel even sends WHO for a newly joined user it's already seen in another channel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Things to squish; generally used for issues Needs Triage Issues that need to be reviewed and categorized
Projects
None yet
Development

No branches or pull requests

2 participants