Add user nick name moderation, and change user nickname to random name - #166
Conversation
PenguinBoi12
left a comment
There was a problem hiding this comment.
It's a pretty good first pass for this feature, most of the big pieces make sense and the general logic is good.
My main concern is about how you implemented validation of name, there's a simpler way to do this which I described in my comments but don't hesitate to ping me on discord about it if you need.
A few others things to look into are names of tests, they are often broad and not really descriptive of what they test.
Good job, it shouldn't take too much work to this ready to approve!
| """ | ||
|
|
||
| # source : https://github.com/moby/moby/blob/master/internal/namesgenerator/names-generator.go | ||
| left = [ |
There was a problem hiding this comment.
nit: Why make 2 lists and not just one that you choose randomly 2 values?
| """ | ||
|
|
||
| # source : https://github.com/moby/moby/blob/master/internal/namesgenerator/names-generator.go | ||
| left = [ |
There was a problem hiding this comment.
nit: I'd prefer having the list of names as private constant outside the function:
_NAMES = [
...
]
def make_random_name() -> str:
...
This keeps the function all about the logic that generates the code and not the data.
|
|
||
|
|
||
| @patch("random.choice") | ||
| def test_random_name_service__expect_normal_output(mock_random): |
There was a problem hiding this comment.
normal_output doesn't really explain what is expected. Instead, describe what is the expected output.
| f"Your name has an inappropriate word in it, thus it was changed from {NAME} to {good_name}." | ||
| ) | ||
|
|
||
| @hybrid_command( |
There was a problem hiding this comment.
Do we really need a command?
| f"Your name has an inappropriate word in it, thus it was changed from {NAME} to {good_name}." | ||
| ) | ||
|
|
||
| @hybrid_command( |
There was a problem hiding this comment.
Do we really need a command to generate name? I don't really see the use case for it. If it's not needed, we should probably remove it to avoid bloating the code for no reason.
| """ | ||
| slices_set = set(slices) | ||
|
|
||
| return not slices_set.isdisjoint(self.BAD_WORDS) |
There was a problem hiding this comment.
Instead of using a reverse logic and a set, you could do:
return any(s in self.BAD_WORDS for s in slices)This is easier to read and understand what its doing.
| from bot.helpers.log_helper import notice | ||
|
|
||
|
|
||
| def slice_name(name: str) -> list[str]: |
There was a problem hiding this comment.
We don't need to slice the name into every possible substring, because Python already has a built-in way to check "is this smaller string somewhere inside this bigger string?", the in operator:
"cat" in "concatenate" # True, "cat" is inside "concatenate"
"dog" in "concatenate" # False, "dog" is not inside "concatenate"
Instead, we can skip the cutting-up step entirely and just ask directly, for each bad word, "is this bad word inside the name?":
def contains_bad_word(self, name: str) -> bool:
lowered = name.lower()
return any(bad_word in lowered for bad_word in self.BAD_WORDS)This loops over BAD_WORDS (which is a short list) instead of looping over every substring of the name (which can be a lot of substrings for a long name, a 20-character name has over 200 possible slices!). Same result, way less work, and the code reads as exactly what it's checking: "does this name contain a bad word?"
| @patch("bot.extensions.name_moderation_cog.notice") | ||
| @patch("bot.extensions.name_moderation_cog.make_random_name") | ||
| @patch("bot.extensions.name_moderation_cog.slice_name") | ||
| async def test_name_moderation_on_member_join__with_normal_name__expect_nothing( |
There was a problem hiding this comment.
nit: with_none_blacklisted_name
| @patch("bot.extensions.name_moderation_cog.notice") | ||
| @patch("bot.extensions.name_moderation_cog.make_random_name") | ||
| @patch("bot.extensions.name_moderation_cog.slice_name") | ||
| async def test_name_moderation_on_member_join__with_bad_name__expect_change_and_message( |
There was a problem hiding this comment.
nit: with_blacklisted_name
| [["Stuff", "Name"], {"Stuff", "Name"}, True], | ||
| ], | ||
| ) | ||
| def test_check_slices__expecgtg_matching_output( |
There was a problem hiding this comment.
I think there's a small typo here. Also, the name isn't descriptive enough.
|
|
||
| def __init__(self, bot: Grace): | ||
| self.bot: Grace = bot | ||
| self.BAD_WORDS = set(app.config.get("name_moderation", "blacklist", "")) |
There was a problem hiding this comment.
Uppercase name are reserved for constant, use lowercased snakecase for attributes and variable names.
|
|
||
| def __init__(self, bot: Grace): | ||
| self.bot: Grace = bot | ||
| self.BAD_WORDS = set(app.config.get("name_moderation", "blacklist", "")) |
There was a problem hiding this comment.
The default should be a list, not an empty string
| class NameModerationCog( | ||
| Cog, name="Names", description="Checks and changes user's nickname." | ||
| ): | ||
| """A cog that checks when a member joins if they have a bad word in their name, and changes their name in case they do.""" |
There was a problem hiding this comment.
Nit: it would be great if you could describe how to correctly configure the cog's blacklist. Just a small example would be enough.
| :rtype: str | ||
| """ | ||
|
|
||
| name = f"{random.choice(_LEFT).capitalize()} {random.choice(_RIGHT).capitalize()}" |
There was a problem hiding this comment.
nit: instead of calling .capitalize() on both name you could call .title() on the whole string.
| @pytest.mark.asyncio | ||
| @patch("bot.extensions.name_moderation_cog.notice") | ||
| @patch("bot.extensions.name_moderation_cog.make_random_name") | ||
| async def test_name_moderation_on_member_join__with_blacklisted_name__expect_change_and_message( |
There was a problem hiding this comment.
nit: don't be afraid to go even more specific test_on_member_join__with_blacklisted_name__expect_changed_name_and_sends_message
Uh oh!
There was an error while loading. Please reload this page.