aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--README.md2
-rw-r--r--bot/cogs/sync/cog.py55
-rw-r--r--bot/cogs/utils.py14
-rw-r--r--bot/cogs/watchchannels/bigbrother.py2
-rw-r--r--bot/cogs/watchchannels/talentpool.py2
-rw-r--r--bot/constants.py6
-rw-r--r--bot/rules/attachments.py4
-rw-r--r--config-default.yml21
-rw-r--r--tests/bot/rules/test_attachments.py110
-rw-r--r--tests/bot/rules/test_links.py26
-rw-r--r--tests/bot/rules/test_mentions.py95
11 files changed, 246 insertions, 91 deletions
diff --git a/README.md b/README.md
index 7a7f1b992..1e7b21271 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Python Utility Bot
-[![Discord](https://img.shields.io/discord/267624335836053506?color=%237289DA&label=Python%20Discord&logo=discord&logoColor=white)](https://discord.gg/2B963hn)
+[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E30k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn)
[![Build Status](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Bot?branchName=master)](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=1&branchName=master)
[![Tests](https://img.shields.io/azure-devops/tests/python-discord/Python%20Discord/1?compact_message)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Bot?branchName=master)
[![Coverage](https://img.shields.io/azure-devops/coverage/python-discord/Python%20Discord/1/master)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Bot?branchName=master)
diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py
index 90d4c40fe..4e6ed156b 100644
--- a/bot/cogs/sync/cog.py
+++ b/bot/cogs/sync/cog.py
@@ -1,7 +1,7 @@
import logging
-from typing import Callable, Iterable
+from typing import Callable, Dict, Iterable, Union
-from discord import Guild, Member, Role
+from discord import Guild, Member, Role, User
from discord.ext import commands
from discord.ext.commands import Cog, Context
@@ -51,6 +51,15 @@ class Sync(Cog):
f"deleted `{total_deleted}`."
)
+ async def patch_user(self, user_id: int, updated_information: Dict[str, Union[str, int]]) -> None:
+ """Send a PATCH request to partially update a user in the database."""
+ try:
+ await self.bot.api_client.patch("bot/users/" + str(user_id), json=updated_information)
+ except ResponseCodeError as e:
+ if e.response.status != 404:
+ raise
+ log.warning("Unable to update user, got 404. Assuming race condition from join event.")
+
@Cog.listener()
async def on_guild_role_create(self, role: Role) -> None:
"""Adds newly create role to the database table over the API."""
@@ -143,33 +152,21 @@ class Sync(Cog):
@Cog.listener()
async def on_member_update(self, before: Member, after: Member) -> None:
- """Updates the user information if any of relevant attributes have changed."""
- if (
- before.name != after.name
- or before.avatar != after.avatar
- or before.discriminator != after.discriminator
- or before.roles != after.roles
- ):
- try:
- await self.bot.api_client.put(
- 'bot/users/' + str(after.id),
- json={
- 'avatar_hash': after.avatar,
- 'discriminator': int(after.discriminator),
- 'id': after.id,
- 'in_guild': True,
- 'name': after.name,
- 'roles': sorted(role.id for role in after.roles)
- }
- )
- except ResponseCodeError as e:
- if e.response.status != 404:
- raise
-
- log.warning(
- "Unable to update user, got 404. "
- "Assuming race condition from join event."
- )
+ """Update the roles of the member in the database if a change is detected."""
+ if before.roles != after.roles:
+ updated_information = {"roles": sorted(role.id for role in after.roles)}
+ await self.patch_user(after.id, updated_information=updated_information)
+
+ @Cog.listener()
+ async def on_user_update(self, before: User, after: User) -> None:
+ """Update the user information in the database if a relevant change is detected."""
+ if any(getattr(before, attr) != getattr(after, attr) for attr in ("name", "discriminator", "avatar")):
+ updated_information = {
+ "name": after.name,
+ "discriminator": int(after.discriminator),
+ "avatar_hash": after.avatar,
+ }
+ await self.patch_user(after.id, updated_information=updated_information)
@commands.group(name='sync')
@commands.has_permissions(administrator=True)
diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index 47a59db66..da278011a 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -62,14 +62,12 @@ class Utils(Cog):
pep_embed.set_thumbnail(url="https://www.python.org/static/opengraph-icon-200x200.png")
# Add the interesting information
- if "Status" in pep_header:
- pep_embed.add_field(name="Status", value=pep_header["Status"])
- if "Python-Version" in pep_header:
- pep_embed.add_field(name="Python-Version", value=pep_header["Python-Version"])
- if "Created" in pep_header:
- pep_embed.add_field(name="Created", value=pep_header["Created"])
- if "Type" in pep_header:
- pep_embed.add_field(name="Type", value=pep_header["Type"])
+ fields_to_check = ("Status", "Python-Version", "Created", "Type")
+ for field in fields_to_check:
+ # Check for a PEP metadata field that is present but has an empty value
+ # embed field values can't contain an empty string
+ if pep_header.get(field, ""):
+ pep_embed.add_field(name=field, value=pep_header[field])
elif response.status != 404:
# any response except 200 and 404 is expected
diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py
index fbee4f5d7..28c1681cf 100644
--- a/bot/cogs/watchchannels/bigbrother.py
+++ b/bot/cogs/watchchannels/bigbrother.py
@@ -62,7 +62,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
return
if user.id in self.watched_users:
- await ctx.send(":x: The specified user is already being watched.")
+ await ctx.send(f":x: {user} is already being watched.")
return
response = await post_infraction(ctx, user, 'watch', reason, hidden=True, active=True)
diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py
index cc8feeeee..f990ccff8 100644
--- a/bot/cogs/watchchannels/talentpool.py
+++ b/bot/cogs/watchchannels/talentpool.py
@@ -69,7 +69,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
return
if user.id in self.watched_users:
- await ctx.send(":x: The specified user is already being watched in the talent pool")
+ await ctx.send(f":x: {user} is already being watched in the talent pool")
return
# Manual request with `raise_for_status` as False because we want the actual response
diff --git a/bot/constants.py b/bot/constants.py
index 8815ab983..2c0e3b10b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -270,6 +270,12 @@ class Emojis(metaclass=YAMLGetter):
ducky_ninja: int
ducky_devil: int
ducky_tube: int
+ ducky_hunt: int
+ ducky_wizard: int
+ ducky_party: int
+ ducky_angel: int
+ ducky_maul: int
+ ducky_santa: int
upvotes: str
comments: str
diff --git a/bot/rules/attachments.py b/bot/rules/attachments.py
index c550aed76..00bb2a949 100644
--- a/bot/rules/attachments.py
+++ b/bot/rules/attachments.py
@@ -7,14 +7,14 @@ async def apply(
last_message: Message, recent_messages: List[Message], config: Dict[str, int]
) -> Optional[Tuple[str, Iterable[Member], Iterable[Message]]]:
"""Detects total attachments exceeding the limit sent by a single user."""
- relevant_messages = [last_message] + [
+ relevant_messages = tuple(
msg
for msg in recent_messages
if (
msg.author == last_message.author
and len(msg.attachments) > 0
)
- ]
+ )
total_recent_attachments = sum(len(msg.attachments) for msg in relevant_messages)
if total_recent_attachments > config['max']:
diff --git a/config-default.yml b/config-default.yml
index 6ae07da93..bf8509544 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -41,6 +41,12 @@ style:
ducky_ninja: &DUCKY_NINJA 637923502535606293
ducky_devil: &DUCKY_DEVIL 637925314982576139
ducky_tube: &DUCKY_TUBE 637881368008851456
+ ducky_hunt: &DUCKY_HUNT 639355090909528084
+ ducky_wizard: &DUCKY_WIZARD 639355996954689536
+ ducky_party: &DUCKY_PARTY 639468753440210977
+ ducky_angel: &DUCKY_ANGEL 640121935610511361
+ ducky_maul: &DUCKY_MAUL 640137724958867467
+ ducky_santa: &DUCKY_SANTA 655360331002019870
upvotes: "<:upvotes:638729835245731840>"
comments: "<:comments:638729835073765387>"
@@ -148,7 +154,7 @@ guild:
contributor: 295488872404484098
core_developer: 587606783669829632
helpers: 267630620367257601
- jammer: 423054537079783434
+ jammer: 591786436651646989
moderator: &MOD_ROLE 267629731250176001
muted: &MUTED_ROLE 277914926603829249
owner: &OWNER_ROLE 267627879762755584
@@ -195,6 +201,9 @@ filter:
- 544525886180032552 # kennethreitz.org
- 590806733924859943 # Discord Hack Week
- 423249981340778496 # Kivy
+ - 197038439483310086 # Discord Testers
+ - 286633898581164032 # Ren'Py
+ - 349505959032389632 # PyGame
domain_blacklist:
- pornhub.com
@@ -362,6 +371,14 @@ anti_malware:
- '.png'
- '.tiff'
- '.wmv'
+ - '.svg'
+ - '.psd' # Photoshop
+ - '.ai' # Illustrator
+ - '.aep' # After Effects
+ - '.xcf' # GIMP
+ - '.mp3'
+ - '.wav'
+ - '.ogg'
reddit:
@@ -400,7 +417,7 @@ redirect_output:
duck_pond:
threshold: 5
- custom_emojis: [*DUCKY_YELLOW, *DUCKY_BLURPLE, *DUCKY_CAMO, *DUCKY_DEVIL, *DUCKY_NINJA, *DUCKY_REGAL, *DUCKY_TUBE]
+ custom_emojis: [*DUCKY_YELLOW, *DUCKY_BLURPLE, *DUCKY_CAMO, *DUCKY_DEVIL, *DUCKY_NINJA, *DUCKY_REGAL, *DUCKY_TUBE, *DUCKY_HUNT, *DUCKY_WIZARD, *DUCKY_PARTY, *DUCKY_ANGEL, *DUCKY_MAUL, *DUCKY_SANTA]
config:
required_keys: ['bot.token']
diff --git a/tests/bot/rules/test_attachments.py b/tests/bot/rules/test_attachments.py
index 4bb0acf7c..d7187f315 100644
--- a/tests/bot/rules/test_attachments.py
+++ b/tests/bot/rules/test_attachments.py
@@ -1,52 +1,98 @@
-import asyncio
import unittest
-from dataclasses import dataclass
-from typing import Any, List
+from typing import List, NamedTuple, Tuple
from bot.rules import attachments
+from tests.helpers import MockMessage, async_test
-# Using `MagicMock` sadly doesn't work for this usecase
-# since it's __eq__ compares the MagicMock's ID. We just
-# want to compare the actual attributes we set.
-@dataclass
-class FakeMessage:
- author: str
- attachments: List[Any]
+class Case(NamedTuple):
+ recent_messages: List[MockMessage]
+ culprit: Tuple[str]
+ total_attachments: int
-def msg(total_attachments: int) -> FakeMessage:
- return FakeMessage(author='lemon', attachments=list(range(total_attachments)))
+def msg(author: str, total_attachments: int) -> MockMessage:
+ """Builds a message with `total_attachments` attachments."""
+ return MockMessage(author=author, attachments=list(range(total_attachments)))
class AttachmentRuleTests(unittest.TestCase):
- """Tests applying the `attachment` antispam rule."""
+ """Tests applying the `attachments` antispam rule."""
- def test_allows_messages_without_too_many_attachments(self):
+ def setUp(self):
+ self.config = {"max": 5}
+
+ @async_test
+ async def test_allows_messages_without_too_many_attachments(self):
"""Messages without too many attachments are allowed as-is."""
cases = (
- (msg(0), msg(0), msg(0)),
- (msg(2), msg(2)),
- (msg(0),),
+ [msg("bob", 0), msg("bob", 0), msg("bob", 0)],
+ [msg("bob", 2), msg("bob", 2)],
+ [msg("bob", 2), msg("alice", 2), msg("bob", 2)],
)
- for last_message, *recent_messages in cases:
- with self.subTest(last_message=last_message, recent_messages=recent_messages):
- coro = attachments.apply(last_message, recent_messages, {'max': 5})
- self.assertIsNone(asyncio.run(coro))
+ for recent_messages in cases:
+ last_message = recent_messages[0]
+
+ with self.subTest(
+ last_message=last_message,
+ recent_messages=recent_messages,
+ config=self.config
+ ):
+ self.assertIsNone(
+ await attachments.apply(last_message, recent_messages, self.config)
+ )
- def test_disallows_messages_with_too_many_attachments(self):
+ @async_test
+ async def test_disallows_messages_with_too_many_attachments(self):
"""Messages with too many attachments trigger the rule."""
cases = (
- ((msg(4), msg(0), msg(6)), [msg(4), msg(6)], 10),
- ((msg(6),), [msg(6)], 6),
- ((msg(1),) * 6, [msg(1)] * 6, 6),
+ Case(
+ [msg("bob", 4), msg("bob", 0), msg("bob", 6)],
+ ("bob",),
+ 10
+ ),
+ Case(
+ [msg("bob", 4), msg("alice", 6), msg("bob", 2)],
+ ("bob",),
+ 6
+ ),
+ Case(
+ [msg("alice", 6)],
+ ("alice",),
+ 6
+ ),
+ (
+ [msg("alice", 1) for _ in range(6)],
+ ("alice",),
+ 6
+ ),
)
- for messages, relevant_messages, total in cases:
- with self.subTest(messages=messages, relevant_messages=relevant_messages, total=total):
- last_message, *recent_messages = messages
- coro = attachments.apply(last_message, recent_messages, {'max': 5})
- self.assertEqual(
- asyncio.run(coro),
- (f"sent {total} attachments in 5s", ('lemon',), relevant_messages)
+
+ for recent_messages, culprit, total_attachments in cases:
+ last_message = recent_messages[0]
+ relevant_messages = tuple(
+ msg
+ for msg in recent_messages
+ if (
+ msg.author == last_message.author
+ and len(msg.attachments) > 0
+ )
+ )
+
+ with self.subTest(
+ last_message=last_message,
+ recent_messages=recent_messages,
+ relevant_messages=relevant_messages,
+ total_attachments=total_attachments,
+ config=self.config
+ ):
+ desired_output = (
+ f"sent {total_attachments} attachments in {self.config['max']}s",
+ culprit,
+ relevant_messages
+ )
+ self.assertTupleEqual(
+ await attachments.apply(last_message, recent_messages, self.config),
+ desired_output
)
diff --git a/tests/bot/rules/test_links.py b/tests/bot/rules/test_links.py
index be832843b..02a5d5501 100644
--- a/tests/bot/rules/test_links.py
+++ b/tests/bot/rules/test_links.py
@@ -2,25 +2,19 @@ import unittest
from typing import List, NamedTuple, Tuple
from bot.rules import links
-from tests.helpers import async_test
-
-
-class FakeMessage(NamedTuple):
- author: str
- content: str
+from tests.helpers import MockMessage, async_test
class Case(NamedTuple):
- recent_messages: List[FakeMessage]
- relevant_messages: Tuple[FakeMessage]
+ recent_messages: List[MockMessage]
culprit: Tuple[str]
total_links: int
-def msg(author: str, total_links: int) -> FakeMessage:
- """Makes a message with *total_links* links."""
+def msg(author: str, total_links: int) -> MockMessage:
+ """Makes a message with `total_links` links."""
content = " ".join(["https://pydis.com"] * total_links)
- return FakeMessage(author=author, content=content)
+ return MockMessage(author=author, content=content)
class LinksTests(unittest.TestCase):
@@ -61,26 +55,28 @@ class LinksTests(unittest.TestCase):
cases = (
Case(
[msg("bob", 1), msg("bob", 2)],
- (msg("bob", 1), msg("bob", 2)),
("bob",),
3
),
Case(
[msg("alice", 1), msg("alice", 1), msg("alice", 1)],
- (msg("alice", 1), msg("alice", 1), msg("alice", 1)),
("alice",),
3
),
Case(
[msg("alice", 2), msg("bob", 3), msg("alice", 1)],
- (msg("alice", 2), msg("alice", 1)),
("alice",),
3
)
)
- for recent_messages, relevant_messages, culprit, total_links in cases:
+ for recent_messages, culprit, total_links in cases:
last_message = recent_messages[0]
+ relevant_messages = tuple(
+ msg
+ for msg in recent_messages
+ if msg.author == last_message.author
+ )
with self.subTest(
last_message=last_message,
diff --git a/tests/bot/rules/test_mentions.py b/tests/bot/rules/test_mentions.py
new file mode 100644
index 000000000..ad49ead32
--- /dev/null
+++ b/tests/bot/rules/test_mentions.py
@@ -0,0 +1,95 @@
+import unittest
+from typing import List, NamedTuple, Tuple
+
+from bot.rules import mentions
+from tests.helpers import MockMessage, async_test
+
+
+class Case(NamedTuple):
+ recent_messages: List[MockMessage]
+ culprit: Tuple[str]
+ total_mentions: int
+
+
+def msg(author: str, total_mentions: int) -> MockMessage:
+ """Makes a message with `total_mentions` mentions."""
+ return MockMessage(author=author, mentions=list(range(total_mentions)))
+
+
+class TestMentions(unittest.TestCase):
+ """Tests applying the `mentions` antispam rule."""
+
+ def setUp(self):
+ self.config = {
+ "max": 2,
+ "interval": 10
+ }
+
+ @async_test
+ async def test_mentions_within_limit(self):
+ """Messages with an allowed amount of mentions."""
+ cases = (
+ [msg("bob", 0)],
+ [msg("bob", 2)],
+ [msg("bob", 1), msg("bob", 1)],
+ [msg("bob", 1), msg("alice", 2)]
+ )
+
+ for recent_messages in cases:
+ last_message = recent_messages[0]
+
+ with self.subTest(
+ last_message=last_message,
+ recent_messages=recent_messages,
+ config=self.config
+ ):
+ self.assertIsNone(
+ await mentions.apply(last_message, recent_messages, self.config)
+ )
+
+ @async_test
+ async def test_mentions_exceeding_limit(self):
+ """Messages with a higher than allowed amount of mentions."""
+ cases = (
+ Case(
+ [msg("bob", 3)],
+ ("bob",),
+ 3
+ ),
+ Case(
+ [msg("alice", 2), msg("alice", 0), msg("alice", 1)],
+ ("alice",),
+ 3
+ ),
+ Case(
+ [msg("bob", 2), msg("alice", 3), msg("bob", 2)],
+ ("bob",),
+ 4
+ )
+ )
+
+ for recent_messages, culprit, total_mentions in cases:
+ last_message = recent_messages[0]
+ relevant_messages = tuple(
+ msg
+ for msg in recent_messages
+ if msg.author == last_message.author
+ )
+
+ with self.subTest(
+ last_message=last_message,
+ recent_messages=recent_messages,
+ relevant_messages=relevant_messages,
+ culprit=culprit,
+ total_mentions=total_mentions,
+ cofig=self.config
+ ):
+ desired_output = (
+ f"sent {total_mentions} mentions in {self.config['interval']}s",
+ culprit,
+ relevant_messages
+ )
+ self.assertTupleEqual(
+ await mentions.apply(last_message, recent_messages, self.config),
+ desired_output
+ )