-
-
Notifications
You must be signed in to change notification settings - Fork 751
Duck pond! #621
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
Merged
Merged
Duck pond! #621
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
7f1d319
Add duck-pond constants.
957f462
Add duck_pond cog.
38579ad
Appease the linter
0957bee
Add correct values for constants from production server.
c66cd6f
Fix broken constant tests
dac975e
Improve the setup() docstring
lemonsaurus 5162222
Addressing review by Mark.
08d0c46
Adding kosas additional ducks to default-config
77429f7
Merge branch 'duck_pond' of github.com:python-discord/bot into duck_pond
a98485e
Figure out which tests we need.
ddeeb12
Resolving merge conflicts from master
acb937d
Test is_staff and has_green_checkmark.
aac8404
Adding ducky count tests and a new AsyncIteratorMock
98ccfbc
Implement a mixed duck test.
lemonsaurus a89349e
Add tests for on_raw_reaction_add.
654646d
Merging in master
ccda39c
Add bot=False default value to MockMember
SebastiaanZ 61051f9
Add MockAttachment type and attachments default for MockMessage
SebastiaanZ 8c64fc6
Check only for bot's green checkmark in DuckPond
SebastiaanZ f56f6ce
Refactor DuckPond msg relay to separate method
SebastiaanZ 89890d6
Move payload checks to start of DuckPond.on_raw_reaction_add
SebastiaanZ 2779a91
Add `return_value` support and assertions to AsyncIteratorMock
SebastiaanZ 2c77288
Add MockUser to mock `discord.User` objects
SebastiaanZ 647370d
Adjust MockReaction for new AsyncIteratorMock protocol
SebastiaanZ b42a7b5
Add MockAsyncWebhook to mock `discord.Webhook` objects
SebastiaanZ a692a95
Add unit tests with full coverage for `bot.cogs.duck_pond`
SebastiaanZ e67822a
Apply suggestions from code review
SebastiaanZ 4be37c1
Move duckpond payload emoji check to method to create testable unit
SebastiaanZ 80d84e1
Apply review comments to duckpond's unit tests
SebastiaanZ File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import logging | ||
| from typing import Optional, Union | ||
|
|
||
| import discord | ||
| from discord import Color, Embed, Member, Message, RawReactionActionEvent, User, errors | ||
| from discord.ext.commands import Bot, Cog | ||
|
|
||
| from bot import constants | ||
| from bot.utils.messages import send_attachments | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class DuckPond(Cog): | ||
| """Relays messages to #duck-pond whenever a certain number of duck reactions have been achieved.""" | ||
|
|
||
| def __init__(self, bot: Bot): | ||
| self.bot = bot | ||
| self.webhook_id = constants.Webhooks.duck_pond | ||
| self.bot.loop.create_task(self.fetch_webhook()) | ||
|
|
||
| async def fetch_webhook(self) -> None: | ||
| """Fetches the webhook object, so we can post to it.""" | ||
| await self.bot.wait_until_ready() | ||
|
|
||
| try: | ||
| self.webhook = await self.bot.fetch_webhook(self.webhook_id) | ||
| except discord.HTTPException: | ||
| log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") | ||
|
|
||
| @staticmethod | ||
| def is_staff(member: Union[User, Member]) -> bool: | ||
| """Check if a specific member or user is staff.""" | ||
| if hasattr(member, "roles"): | ||
| for role in member.roles: | ||
| if role.id in constants.STAFF_ROLES: | ||
| return True | ||
| return False | ||
|
|
||
| async def has_green_checkmark(self, message: Message) -> bool: | ||
| """Check if the message has a green checkmark reaction.""" | ||
| for reaction in message.reactions: | ||
| if reaction.emoji == "✅": | ||
| async for user in reaction.users(): | ||
| if user == self.bot.user: | ||
| return True | ||
| return False | ||
|
|
||
| async def send_webhook( | ||
| self, | ||
| content: Optional[str] = None, | ||
| username: Optional[str] = None, | ||
| avatar_url: Optional[str] = None, | ||
| embed: Optional[Embed] = None, | ||
| ) -> None: | ||
| """Send a webhook to the duck_pond channel.""" | ||
| try: | ||
| await self.webhook.send( | ||
| content=content, | ||
| username=username, | ||
| avatar_url=avatar_url, | ||
| embed=embed | ||
| ) | ||
| except discord.HTTPException: | ||
| log.exception("Failed to send a message to the Duck Pool webhook") | ||
|
|
||
| async def count_ducks(self, message: Message) -> int: | ||
| """ | ||
| Count the number of ducks in the reactions of a specific message. | ||
|
|
||
| Only counts ducks added by staff members. | ||
| """ | ||
| duck_count = 0 | ||
| duck_reactors = [] | ||
|
|
||
| for reaction in message.reactions: | ||
| async for user in reaction.users(): | ||
|
|
||
| # Is the user a staff member and not already counted as reactor? | ||
| if not self.is_staff(user) or user.id in duck_reactors: | ||
| continue | ||
|
|
||
| # Is the emoji a duck? | ||
| if hasattr(reaction.emoji, "id"): | ||
| if reaction.emoji.id in constants.DuckPond.custom_emojis: | ||
| duck_count += 1 | ||
| duck_reactors.append(user.id) | ||
| elif isinstance(reaction.emoji, str): | ||
| if reaction.emoji == "🦆": | ||
| duck_count += 1 | ||
| duck_reactors.append(user.id) | ||
| return duck_count | ||
|
|
||
| async def relay_message(self, message: Message) -> None: | ||
| """Relays the message's content and attachments to the duck pond channel.""" | ||
| clean_content = message.clean_content | ||
|
|
||
| if clean_content: | ||
| await self.send_webhook( | ||
| content=message.clean_content, | ||
| username=message.author.display_name, | ||
| avatar_url=message.author.avatar_url | ||
| ) | ||
|
|
||
| if message.attachments: | ||
| try: | ||
| await send_attachments(message, self.webhook) | ||
| except (errors.Forbidden, errors.NotFound): | ||
| e = Embed( | ||
| description=":x: **This message contained an attachment, but it could not be retrieved**", | ||
| color=Color.red() | ||
| ) | ||
| await self.send_webhook( | ||
| embed=e, | ||
| username=message.author.display_name, | ||
| avatar_url=message.author.avatar_url | ||
| ) | ||
| except discord.HTTPException: | ||
| log.exception(f"Failed to send an attachment to the webhook") | ||
|
|
||
| await message.add_reaction("✅") | ||
|
|
||
| @staticmethod | ||
| def _payload_has_duckpond_emoji(payload: RawReactionActionEvent) -> bool: | ||
| """Test if the RawReactionActionEvent payload contains a duckpond emoji.""" | ||
| if payload.emoji.is_custom_emoji(): | ||
| if payload.emoji.id in constants.DuckPond.custom_emojis: | ||
| return True | ||
| elif payload.emoji.name == "🦆": | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
| @Cog.listener() | ||
| async def on_raw_reaction_add(self, payload: RawReactionActionEvent) -> None: | ||
| """ | ||
| Determine if a message should be sent to the duck pond. | ||
|
|
||
| This will count the number of duck reactions on the message, and if this amount meets the | ||
| amount of ducks specified in the config under duck_pond/threshold, it will | ||
| send the message off to the duck pond. | ||
| """ | ||
| # Is the emoji in the reaction a duck? | ||
| if not self._payload_has_duckpond_emoji(payload): | ||
| return | ||
|
|
||
| channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) | ||
| message = await channel.fetch_message(payload.message_id) | ||
| member = discord.utils.get(message.guild.members, id=payload.user_id) | ||
|
|
||
| # Is the member a human and a staff member? | ||
| if not self.is_staff(member) or member.bot: | ||
| return | ||
|
|
||
| # Does the message already have a green checkmark? | ||
| if await self.has_green_checkmark(message): | ||
| return | ||
|
|
||
| # Time to count our ducks! | ||
| duck_count = await self.count_ducks(message) | ||
|
|
||
| # If we've got more than the required amount of ducks, send the message to the duck_pond. | ||
| if duck_count >= constants.DuckPond.threshold: | ||
| await self.relay_message(message) | ||
|
|
||
| @Cog.listener() | ||
| async def on_raw_reaction_remove(self, payload: RawReactionActionEvent) -> None: | ||
| """Ensure that people don't remove the green checkmark from duck ponded messages.""" | ||
| channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) | ||
|
|
||
| # Prevent the green checkmark from being removed | ||
| if payload.emoji.name == "✅": | ||
| message = await channel.fetch_message(payload.message_id) | ||
| duck_count = await self.count_ducks(message) | ||
| if duck_count >= constants.DuckPond.threshold: | ||
| await message.add_reaction("✅") | ||
|
|
||
|
|
||
| def setup(bot: Bot) -> None: | ||
| """Load the duck pond cog.""" | ||
| bot.add_cog(DuckPond(bot)) | ||
| log.info("Cog loaded: DuckPond") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.