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

bot, coretasks, irc, plugin, plugins: new capability negotiation system #2341

Merged
merged 16 commits into from
Mar 21, 2023

Conversation

Exirel
Copy link
Contributor

@Exirel Exirel commented Aug 13, 2022

Description

Fix #972 and should fix #974 as well.

I rewrote the entire capability negotiation feature.
I haven't tested it live properly, and I want to add so much more tests, this will have to wait.

Don't review the docstring yet or you are going to have a bad time, as I'm going to rebase/squash this multiple times until I'm happy with the result.

This PR exists so anyone can take a chance to test it live on their own setup.

Checklist

  • I have read CONTRIBUTING.md
  • I can and do license this contribution under the EFLv2
  • No issues are reported by make qa (runs make quality and make test)
  • I have tested the functionality of the things this change touches

@Exirel Exirel added this to the 8.0.0 milestone Aug 13, 2022
@Exirel Exirel force-pushed the plugin-cap-decorator branch 2 times, most recently from 9f8b81b to f3ff776 Compare August 14, 2022 17:57
@Exirel
Copy link
Contributor Author

Exirel commented Aug 14, 2022

So, it's far from it, but it's getting there:

  • I've added way more tests, both for sasl support and cap request en general
  • I've find a way to make the system more or less backward compatible, with some caveat, with bot.cap_req() (which is a nightmare)
  • I still need to write some good documentation on how to use all of that, probably in the "advanced tips and tricks" because cap negotiation is not for the faint of heart

We are reaching slowly, but surely, the 60% coverage rate!

Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm going to stop this round before I get absolutely lost in test/. There's a lot of new stuff in there, but as long as the suite passes there's no reason to spend time looking in there yet.

Some big "what if" and "why not" questions from me. I'll probably be excoriated for even thinking some of them—and with good reason 😁—but I hope that overall, this collection of feedback will be useful in polishing the approach here.

sopel/bot.py Outdated Show resolved Hide resolved
sopel/bot.py Outdated Show resolved Hide resolved
sopel/bot.py Outdated Show resolved Hide resolved
sopel/bot.py Outdated Show resolved Hide resolved
sopel/bot.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Show resolved Hide resolved
sopel/plugins/capabilities.py Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Show resolved Hide resolved
@Exirel
Copy link
Contributor Author

Exirel commented Sep 20, 2022

Some big "what if" and "why not" questions from me. I'll probably be excoriated for even thinking some of them—and with good reason 😁—but I hope that overall, this collection of feedback will be useful in polishing the approach here.

I took the time to read everything, and I think I tried to reply to most of the important things.

I think most of the questions are about a very specific issue with the protocol's specification: what does it mean to validate a request? Is a request that disable something not available is valid, or not valid?

single cap request vs multi cap request

It is very easy to deal with single-cap request: it either works, or it doesn't. The problems come only when there are multi-cap requests:

  • cap1 cap2
  • cap1 -cap2
  • -cap1 -cap2

From reading the spec, it is entirely possible for a server to NAK the request cap1 cap2 cap3, while ACK three independent requests (one for each cap). Does it make sense? Not really, but that doesn't change the fact that these are not the same.

That's also why I don't want Sopel to send one single CAP REQ with all the capability at once, because that's not how the bot uses these capabilities, and it makes no sense to ACK/NAK all of them as a whole.

callback anti-pattern

While working on that, I discovered some sort of anti-pattern: registering a handler for a CAP REQ that changes the state of the bot (or its memory), or load something. This is, except for one exception, a bad idea.

The proper way to handle when a capability is enabled or disabled is to use the method bot.capabilities.is_enabled() whenever it is needed, instead of storing that information in bot.memory (or a side-effect). It looks cool, but it is actually more a problem than anything else.

It is especially true for capability that are not available, so the bot should not request them, and so it won't received either an ACK nor a NAK. In that case, the callback won't be called.

The good reasons I've seen so far are:

I think we should agree on that, or find a consensus about that, and then document it (probably in an "advanced" chapter of the documentation).

@dgw
Copy link
Member

dgw commented Sep 20, 2022

The good reasons I've seen so far are:

Being able to interact directly with the IRC protocol, as in the case of SASL, is the primary reason that CAP requests need a way to register callbacks, and specifically a way to delay the end of CAP negotiation until the request has done whatever else it needs to do. That's a core function, of course, and so it needs to be supported somehow.

All of this capability-negotiation stuff is already way above the pay grade of most plugin authors, who will never need to know how to use it. It's fine for an "advanced" chapter, likely one with judicious use of .. warning and other admonitions to guide the adventurous souls who need this tool.

There's also no reason not to mark the whole capability request / manager system as provisional, "subject to rapid changes between versions" and all that jazz like we say about the internal plugin machinery, so we have the freedom to iterate on it if necessary without having to think about deprecations. (If including provisional modules in a few releases before marking them as stable is good enough for CPython, it's good enough for us!)

@Exirel
Copy link
Contributor Author

Exirel commented Nov 1, 2022

That's a core function, of course, and so it needs to be supported somehow.

And it is! So we are good on that front.

I've re-read all the comments, and now I think I'm confident with my current design. I have some improvements in mind, such as:

  • allowing a callback for non-advertised CAP
  • allowing conditional CAP REQ, i.e. "don't actually send this CAP REQ if config/env is not OK", to prevent requesting SASL when the configuration doesn't need it, for example
  • an advanced documentation chapter for CAP REQ handling

Given your blessing to make this as provisional, I may suggest to just go with it for now, and see how we can improve that further. For instance, even thought I'm against, working around the "this CAP is not available so -CAP should not be a problem" thing is something that we may want to work on. We could also change the way to declare CAP REQ in a plugin code.

I'll try to fix all the spelling & docstring issues today.

Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scary line note count, but it's mostly typo fixes.

You were right when you said on IRC that this was waiting on me taking another look. I think for my part, when looking for things that needed attention in the PR list, I have been skipping anything that's still marked as a draft. It's probably time to Ready this one after this set of comments, isn't it?

docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
@Exirel Exirel marked this pull request as ready for review January 3, 2023 18:08
@Exirel
Copy link
Contributor Author

Exirel commented Jan 3, 2023

Everything that was typo-related has been fixed. Let me know if you want me to squash this before doing another review, other than that, it's ready on my end.

(and so so so sorry for the long delay, between late November and today, a lot happened on my side and I wasn't able to make this PR to move forward as I intended)

Copy link
Contributor

@SnoopJ SnoopJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting my second-opinion now, without having gone through the test suite with a fine-toothed comb.

Based on the number of my remarks that ended up being nitpicks and docs, I say this looks good to me. Some parts are a little tricky to follow but overall I 'get' it.

docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
sopel/plugin.py Outdated Show resolved Hide resolved
sopel/plugin.py Show resolved Hide resolved
sopel/plugins/handlers.py Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
sopel/irc/__init__.py Outdated Show resolved Hide resolved
sopel/coretasks.py Outdated Show resolved Hide resolved
sopel/irc/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
Copy link
Contributor

@SnoopJ SnoopJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no nits left to pick, this LGTM.

sopel/plugin.py Show resolved Hide resolved
Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of nits that can be handled during squash, but we should make a decision about one apparently unresolved thread that led into discussion about CAP REQs that are too long to be acknowledged in one line. It's fine to say that's for a later patch, but in that case let's have a follow-up issue about it before merging. 😸

docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
sopel/irc/capabilities.py Show resolved Hide resolved
sopel/plugins/capabilities.py Show resolved Hide resolved
@Exirel
Copy link
Contributor Author

Exirel commented Mar 19, 2023

Oof. Done with the third? (I didn't count) round. I think it's the third, or maybe the fourth...

Anyway, I added the limitation on the CAP REQ size, I think it's a very unlikely scenario, however Sopel deals with it now, and there are tests to prove it.

Permission to rebase & squash requested! 😁

Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just style & typo nits. Any style mishaps I've missed at this point will be allowed to live to see the day when a relevant flake8 plugin is added and flags them for correction, if ever.

I'm still worried a bit about the possibility of (as @SnoopJ called it) "a particularly antisocial capability" named with a leading hyphen that would be stripped in plugins.capabilities.Manager.request_available(), but that's a concern that needs to be run by the IRCv3 folks. If it's a possibility, a small patch later can try to account for it. No need to hold this any longer on account of such a wildly unlikely case.

sopel/plugin.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugin.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
@Exirel
Copy link
Contributor Author

Exirel commented Mar 20, 2023

such a wildly unlikely case

I have honestly never think that it would be possible. There is nothing that technically prevent that from happening for standard capabilities, however they should start as a draft first, so people will see the problem quickly.

As for vendor capabilities:

These names are prefixed by a valid DNS domain name.

And a valid DNS domain name MUST NOT start with an hyphen, and in case there is a non-ASCII character in there:

In cases where the prefix contains non-ASCII characters, punycode MUST be used, e.g. xn--e1afmkfd.org/foo.

So I think the spec covers the problem by not allowing anyone to do that in practice.

Also, PR is ready again!

@Exirel Exirel requested a review from dgw March 20, 2023 10:54
Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go-go gadget SQUASHER!

Exirel and others added 14 commits March 20, 2023 22:11
The `sopel.irc.capabilities` submodule aims to implement part of the
IRCv3 specification for Capability Negotiation. Its goal is to store
what the bot knows about capabilities: what is available and what is
enabled.

This submodule doesn't manage capability requests.

Co-authored-by: dgw <[email protected]>
Co-authored-by: James Gerity <[email protected]>
The `capability` decorator, and the `CapabilityNegotiation` enum allow a
plugin to request capabilities.

This will replace the current system with the CapReq class and the
irc.AbstractBot.cap_req method that nobody uses because it's way too
complex and convoluted (also it doesn't do what it says it does, or not
properly).

Collecting capability requests and handling them will be part of another
commit.

Co-authored-by: dgw <[email protected]>
Third parts of the new Capability Negotiation implementation: having a
way to track capability requests and their resolution.

Next stop: using these three parts together into the bot and coretasks.

Co-authored-by: dgw <[email protected]>
Co-authored-by: James Gerity <[email protected]>
This is the last part of the rework: actually use all the right tools
for the job, and deprecate/remove the old wonky ones.

Namely:

* move `bot.cap_req` from `AbstractBot` to `Sopel`, and deprecate it
* replace various attributes by properties from the `irc.capabilities`
  manager
* add the cap request manager (`bot.cap_requests`), and use it to know
  more about the capability requests, and to register requests
* deprecate `irc.utils.CapReq`
* completely rework `sopel.coretasks`'s management of capability
  negotiation, and also SASL authentication
* add so **many** tests for coretasks (CAP & SASL related)

Co-authored-by: dgw <[email protected]>
def test_capability_handler_define_once():
@plugin.capability('away-notify')
def handler(name, bot, acknowledged):
...

Check notice

Code scanning / CodeQL

Statement has no effect

This statement has no effect.
Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately for @Exirel I went through the whole thing* again and found a few more nitpicks—but more importantly, I found an example in one docstring that seems to need clarification.

* — Except for tests. I didn't reread most of the test/ files.

sopel/irc/__init__.py Show resolved Hide resolved
sopel/irc/__init__.py Show resolved Hide resolved
sopel/irc/capabilities.py Show resolved Hide resolved
sopel/irc/capabilities.py Show resolved Hide resolved
sopel/irc/capabilities.py Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugins/capabilities.py Outdated Show resolved Hide resolved
sopel/plugin.py Outdated Show resolved Hide resolved
docs/source/plugin/advanced.rst Outdated Show resolved Hide resolved
sopel/irc/__init__.py Show resolved Hide resolved
Exirel and others added 2 commits March 21, 2023 07:54
Both bot attributes have been deprecated in 8.0, add warnings in 8.1,
to be removed in 9.0:

* `bot.enabled_capabilities`
* `bot.server_capabilities`

Co-authored-by: dgw <[email protected]>
Copy link
Member

@dgw dgw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the revised example makes more sense and we've cleared up everything else too. 🥳

@dgw dgw merged commit f8162f7 into sopel-irc:master Mar 21, 2023
@Exirel Exirel deleted the plugin-cap-decorator branch April 8, 2023 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Stuff that probably should be mentioned in a migration guide Core/IRC Protocol Handling Core/Plugin Handling Feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Stuck if configured to use SASL but server NAKs the capability Sopel blindly requests unadvertised CAPs
3 participants