From a07366631888d372abc58759418cddafce9bdc9e Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 21 Sep 2019 20:31:29 +0100 Subject: Add role info command --- bot/cogs/information.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index c4aff73b8..f05505902 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -1,8 +1,9 @@ +import colorsys import logging import textwrap -from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel -from discord.ext.commands import Bot, Cog, Context, command +from discord import CategoryChannel, Colour, Embed, Member, Role, TextChannel, VoiceChannel +from discord.ext.commands import Bot, Cog, Context, Greedy, command from bot.constants import Channels, Emojis, MODERATION_ROLES, STAFF_ROLES from bot.decorators import InChannelCheckFailure, with_role @@ -50,6 +51,36 @@ class Information(Cog): await ctx.send(embed=embed) + @with_role(*MODERATION_ROLES) + @command(name="role") + async def role_info(self, ctx: Context, roles: Greedy[Role]): + """ + Return information on a role or list of roles. + + To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks. + """ + for role in roles: + embed = Embed( + title=f"{role.name} info", + colour=role.colour, + ) + + embed.add_field(name="ID", value=role.id, inline=True) + + embed.add_field(name="Colour (RGB)", value=f"#{role.colour.value:0>6x}", inline=True) + + h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb()) + + embed.add_field(name="Colour (HSV)", value=f"{h:.2f} {s:.2f} {v:.2f}", inline=True) + + embed.add_field(name="Member count", value=len(role.members), inline=True) + + embed.add_field(name="Position", value=role.position) + + embed.add_field(name="Permission code", value=role.permissions.value, inline=True) + + await ctx.send(embed=embed) + @command(name="server", aliases=["server_info", "guild", "guild_info"]) async def server_info(self, ctx: Context): """ -- cgit v1.2.3 From 2f6379f52ca2b74ad72545b6bb8196da410959e7 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 21 Sep 2019 21:04:16 +0100 Subject: Add unit tests for role info command --- bot/cogs/information.py | 2 +- tests/cogs/test_information.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index f05505902..2dd56333f 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -71,7 +71,7 @@ class Information(Cog): h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb()) - embed.add_field(name="Colour (HSV)", value=f"{h:.2f} {s:.2f} {v:.2f}", inline=True) + embed.add_field(name="Colour (HSV)", value=f"{h:.2f} {s:.2f} {v}", inline=True) embed.add_field(name="Member count", value=len(role.members), inline=True) diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py index 85b2d092e..986e73a65 100644 --- a/tests/cogs/test_information.py +++ b/tests/cogs/test_information.py @@ -8,6 +8,7 @@ import pytest from discord import ( CategoryChannel, Colour, + Permissions, TextChannel, VoiceChannel, ) @@ -66,6 +67,52 @@ def test_roles_info_command(cog, ctx): assert embed.footer.text == "Total roles: 1" +def test_role_info_command(cog, ctx): + dummy_role = MagicMock() + dummy_role.name = "Dummy" + dummy_role.colour = Colour.blurple() + dummy_role.id = 112233445566778899 + dummy_role.position = 10 + dummy_role.permissions = Permissions(0) + dummy_role.members = [ctx.author] + + admin_role = MagicMock() + admin_role.name = "Admin" + admin_role.colour = Colour.red() + admin_role.id = 998877665544332211 + admin_role.position = 3 + admin_role.permissions = Permissions(0) + admin_role.members = [ctx.author] + + ctx.guild.roles = [dummy_role, admin_role] + + cog.role_info.can_run = AsyncMock() + cog.role_info.can_run.return_value = True + + coroutine = cog.role_info.callback(cog, ctx, [dummy_role, admin_role]) + + assert asyncio.run(coroutine) is None + + assert ctx.send.call_count == 2 + + (_, dummy_kwargs), (_, admin_kwargs) = ctx.send.call_args_list + + dummy_embed = dummy_kwargs["embed"] + admin_embed = admin_kwargs["embed"] + + assert dummy_embed.title == "Dummy info" + assert dummy_embed.colour == Colour.blurple() + + assert dummy_embed.fields[0].value == str(dummy_role.id) + assert dummy_embed.fields[1].value == f"#{dummy_role.colour.value:0>6x}" + assert dummy_embed.fields[2].value == "0.63 0.48 218" + assert dummy_embed.fields[3].value == "1" + assert dummy_embed.fields[4].value == "10" + assert dummy_embed.fields[5].value == "0" + + assert admin_embed.title == "Admin info" + assert admin_embed.colour == Colour.red() + # There is no argument passed in here that we can use to test, # so the return value would change constantly. @patch('bot.cogs.information.time_since') -- cgit v1.2.3 From 84c87597ba585ba2612b7174cd5180c6403fb58b Mon Sep 17 00:00:00 2001 From: Tom Sputz Date: Mon, 30 Sep 2019 22:42:24 +0100 Subject: Forward arguments to get --- bot/cogs/doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index e5c51748f..4fadcb8db 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -261,7 +261,7 @@ class Doc(commands.Cog): @commands.group(name='docs', aliases=('doc', 'd'), invoke_without_command=True) async def docs_group(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None: """Lookup documentation for Python symbols.""" - await ctx.invoke(self.get_command) + await self.get_command(ctx, symbol) @docs_group.command(name='get', aliases=('g',)) async def get_command(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None: -- cgit v1.2.3 From 11451fb010eb3afdc48ddcc4ab13fa24614920a0 Mon Sep 17 00:00:00 2001 From: sco1 Date: Mon, 30 Sep 2019 21:33:39 -0400 Subject: Update contrib doc for new wiki links --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0a1200ec..41df8fa20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,9 +36,10 @@ All projects evolve over time, and this contribution guide is no different. This ## Supplemental Information ### Developer Environment -A working environment for the [PyDis site](https://github.com/python-discord/site) is required to develop the bot. Instructions for setting up environments for both the site and the bot can be found on the PyDis Wiki: - * [Site](https://wiki.pythondiscord.com/wiki/contributing/project/site) - * [Bot](https://wiki.pythondiscord.com/wiki/contributing/project/bot) +Instructions for setting the bot developer environment can be found on the PyDis site: + * [Bot](https://pythondiscord.com/pages/contributing/bot/) + +To provide a standalone development environment for this project, docker compose is utilized to pull the current version of the [site backend](https://github.com/python-discord/site). While appropriate for bot-only contributions, any contributions that necessitate backend changes will require the site repository to be appropriately configured as well. Instructions for setting up the site environment can be found on the [PyDis site](https://pythondiscord.com/pages/contributing/site/). When pulling down changes from GitHub, remember to sync your environment using `pipenv sync --dev` to ensure you're using the most up-to-date versions the project's dependencies. -- cgit v1.2.3 From 20da07562aab1d9041170f70e1a3dc086f5c1b90 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 1 Oct 2019 10:53:18 +0200 Subject: Add converter for ISO-formatted datetime strings Related to https://github.com/python-discord/bot/issues/458 This commit adds a converter that automatically parses ISO-formatted datetime strings and returns a `datetime.datetime` object. It uses `dateutil.parser.isoparse` to do the heavy lifting, so it supports the same formats as this method. In addition, I have added tests that ensure that it accepts certain formats and added a description of these 'guaranteed' formats to the `ISODate.convert` docstring. This commit should make it easy to implement #485 --- bot/converters.py | 33 +++++++++++++++++++++++++++++ tests/test_converters.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/bot/converters.py b/bot/converters.py index 339da7b60..49ac488f4 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -4,6 +4,7 @@ from datetime import datetime from ssl import CertificateError from typing import Union +import dateutil.parser import discord from aiohttp import ClientConnectorError from dateutil.relativedelta import relativedelta @@ -215,3 +216,35 @@ class Duration(Converter): now = datetime.utcnow() return now + delta + + +class ISODateTime(Converter): + """"Converts an ISO-8601 datetime string into a datetime.datetime.""" + + async def convert(self, ctx: Context, datetime_string: str) -> datetime: + """ + Converts a ISO-8601 `datetime_string` into a `datetime.datetime` object. + + The converter is flexible in the formats it accepts, as it uses the `isoparse` method of + `dateutil.parser`. In general, it accepts datetime strings that start with a date, + optionally followed by a time. + + See: + + Formats that are guaranteed to be valid by our tests are: + + - `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS` + - `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM` + - `YYYY-mm-dd` + - `YYYY-mm` + - `YYYY` + + Note: ISO-8601 specifies a `T` as the separator between the date and the time part of the + datetime string. The converter accepts both a `T` and a single space character. + """ + try: + dt = dateutil.parser.isoparse(datetime_string) + except ValueError: + raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string") + + return dt diff --git a/tests/test_converters.py b/tests/test_converters.py index 35fc5d88e..aa692f9f8 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -8,6 +8,7 @@ from discord.ext.commands import BadArgument from bot.converters import ( Duration, + ISODateTime, TagContentConverter, TagNameConverter, ValidPythonIdentifier, @@ -184,3 +185,57 @@ def test_duration_converter_for_invalid(duration: str): converter = Duration() with pytest.raises(BadArgument, match=f'`{duration}` is not a valid duration string.'): asyncio.run(converter.convert(None, duration)) + + +@pytest.mark.parametrize( + ("datetime_string", "expected_dt"), + ( + # `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS` + ('2019-09-02T02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), + + # `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM` + ('2019-11-12T09:15', datetime.datetime(2019, 11, 12, 9, 15)), + ('2019-11-12 09:15', datetime.datetime(2019, 11, 12, 9, 15)), + + # `YYYY-mm-dd` + ('2019-04-01', datetime.datetime(2019, 4, 1)), + + # `YYYY-mm` + ('2019-02-01', datetime.datetime(2019, 2, 1)), + + # `YYYY` + ('2025', datetime.datetime(2025, 1, 1)), + ), +) +def test_isodatetime_converter_for_valid(datetime_string: str, expected_dt: datetime.datetime): + converter = ISODateTime() + assert asyncio.run(converter.convert(None, datetime_string)) == expected_dt + + +@pytest.mark.parametrize( + ("datetime_string"), + ( + # Make sure it doesn't interfere with the Duration converation + ('1Y'), + ('1d'), + ('1H'), + + # Check if it fails when only providing the optional time part + ('10:10:10'), + ('10:00'), + + # Invalid date format + ('19-01-01'), + + # Other non-valid strings + ('fisk the tag master'), + ), +) +def test_isodatetime_converter_for_invalid(datetime_string: str): + converter = ISODateTime() + with pytest.raises( + BadArgument, + match=f"`{datetime_string}` is not a valid ISO-8601 datetime string", + ): + asyncio.run(converter.convert(None, datetime_string)) -- cgit v1.2.3 From 4c3714bf406ff750ddc306457f41b4871160338f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 1 Oct 2019 11:34:24 +0200 Subject: Make `!tags edit` use PATCH to update tags https://github.com/python-discord/bot/issues/474 This commit makes sure we're using the PATCH method to update tags, instead of the POST method. The problem was that the `!tags edit` command was an alias of `!tags set`, which uses the POST method to post new tags to the API. However, when a tag with a given name already exists, the API will refuse a POST request in an attempt to update it; we need to use the PATCH method to the `bot/tags/{tag_name}` endpoint instead. I fixed this by creating a separate subcommand, `!tags edit`, that uses the correct PATCH method to update an existing tag. This commit closes #474 --- bot/cogs/tags.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index b9dd3595e..cd70e783a 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -86,7 +86,7 @@ class Tags(Cog): max_lines=15 ) - @tags_group.command(name='set', aliases=('add', 'edit', 's')) + @tags_group.command(name='set', aliases=('add', 's')) @with_role(*MODERATION_ROLES) async def set_command( self, @@ -95,7 +95,7 @@ class Tags(Cog): *, tag_content: TagContentConverter, ) -> None: - """Create a new tag or update an existing one.""" + """Create a new tag.""" body = { 'title': tag_name.lower().strip(), 'embed': { @@ -116,6 +116,35 @@ class Tags(Cog): colour=Colour.blurple() )) + @tags_group.command(name='edit', aliases=('e', )) + @with_role(*MODERATION_ROLES) + async def edit_command( + self, + ctx: Context, + tag_name: TagNameConverter, + *, + tag_content: TagContentConverter, + ) -> None: + """Edit an existing tag.""" + body = { + 'embed': { + 'title': tag_name, + 'description': tag_content + } + } + + await self.bot.api_client.patch(f'bot/tags/{tag_name}', json=body) + + log.debug(f"{ctx.author} successfully edited the following tag in our database: \n" + f"tag_name: {tag_name}\n" + f"tag_content: '{tag_content}'\n") + + await ctx.send(embed=Embed( + title="Tag successfully edited", + description=f"**{tag_name}** edited in the database.", + colour=Colour.blurple() + )) + @tags_group.command(name='delete', aliases=('remove', 'rm', 'd')) @with_role(Roles.admin, Roles.owner) async def delete_command(self, ctx: Context, *, tag_name: TagNameConverter) -> None: -- cgit v1.2.3 From 35c4d06a1807bbc491a75459d8bf7790d1467731 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Tue, 1 Oct 2019 09:40:28 -0400 Subject: Remove single-item list It looks silly --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41df8fa20..39f76c7b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,8 +36,7 @@ All projects evolve over time, and this contribution guide is no different. This ## Supplemental Information ### Developer Environment -Instructions for setting the bot developer environment can be found on the PyDis site: - * [Bot](https://pythondiscord.com/pages/contributing/bot/) +Instructions for setting the bot developer environment can be found on the [PyDis wiki](https://pythondiscord.com/pages/contributing/bot/) To provide a standalone development environment for this project, docker compose is utilized to pull the current version of the [site backend](https://github.com/python-discord/site). While appropriate for bot-only contributions, any contributions that necessitate backend changes will require the site repository to be appropriately configured as well. Instructions for setting up the site environment can be found on the [PyDis site](https://pythondiscord.com/pages/contributing/site/). -- cgit v1.2.3 From 3bb82073773e50807c3aa5aa00a79e402cd451a6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 1 Oct 2019 16:17:42 +0200 Subject: Remove surplus quotation mark in class docstring Co-Authored-By: S. Co1 --- bot/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index 49ac488f4..0bd50c14c 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -219,7 +219,7 @@ class Duration(Converter): class ISODateTime(Converter): - """"Converts an ISO-8601 datetime string into a datetime.datetime.""" + """Converts an ISO-8601 datetime string into a datetime.datetime.""" async def convert(self, ctx: Context, datetime_string: str) -> datetime: """ -- cgit v1.2.3 From ccd03e48896eaca98add143538221cbd671aef4f Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 1 Oct 2019 16:13:30 +0100 Subject: Implement review comments and stop using a greedy converter --- bot/cogs/information.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 2dd56333f..624097eb1 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -1,9 +1,10 @@ import colorsys import logging import textwrap +import typing -from discord import CategoryChannel, Colour, Embed, Member, Role, TextChannel, VoiceChannel -from discord.ext.commands import Bot, Cog, Context, Greedy, command +from discord import CategoryChannel, Colour, Embed, Member, Role, TextChannel, VoiceChannel, utils +from discord.ext.commands import Bot, Cog, Context, command from bot.constants import Channels, Emojis, MODERATION_ROLES, STAFF_ROLES from bot.decorators import InChannelCheckFailure, with_role @@ -53,13 +54,30 @@ class Information(Cog): @with_role(*MODERATION_ROLES) @command(name="role") - async def role_info(self, ctx: Context, roles: Greedy[Role]): + async def role_info(self, ctx: Context, *roles: typing.Union[Role, str]): """ Return information on a role or list of roles. To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks. """ - for role in roles: + parsed_roles = [] + + for role_name in roles: + if isinstance(role_name, Role): + # Role conversion has already succeeded + parsed_roles.append(role_name) + continue + + role = utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) + + if not role: + await ctx.send(f":x: Could not convert `{role_name}` to a role") + continue + + parsed_roles.append(role) + + + for role in parsed_roles: embed = Embed( title=f"{role.name} info", colour=role.colour, -- cgit v1.2.3 From 9b1ffd1f1fe2f2e0d7a335aa0d50b4c1717eb056 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 1 Oct 2019 16:14:24 +0100 Subject: linter is the bane of my existence --- bot/cogs/information.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 624097eb1..c789dbcc0 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -75,7 +75,6 @@ class Information(Cog): continue parsed_roles.append(role) - for role in parsed_roles: embed = Embed( -- cgit v1.2.3 From 9fff510e0ba493760a14728f2c71fb338b3d8460 Mon Sep 17 00:00:00 2001 From: Ayplo Date: Tue, 1 Oct 2019 16:30:50 +0100 Subject: Update bot/cogs/doc.py Co-Authored-By: S. Co1 --- bot/cogs/doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 4fadcb8db..40b31f90c 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -261,7 +261,7 @@ class Doc(commands.Cog): @commands.group(name='docs', aliases=('doc', 'd'), invoke_without_command=True) async def docs_group(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None: """Lookup documentation for Python symbols.""" - await self.get_command(ctx, symbol) + await ctx.invoke(self.get_command, symbol) @docs_group.command(name='get', aliases=('g',)) async def get_command(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None: -- cgit v1.2.3 From 7bc7be9a6f1e60913d0bbbde8980af657e493177 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Tue, 1 Oct 2019 11:34:21 -0400 Subject: Update docs cog docstring & fix URL converter attribute error --- bot/cogs/doc.py | 6 +++--- bot/converters.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 40b31f90c..c9e6b3b91 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -319,9 +319,9 @@ class Doc(commands.Cog): Example: !docs set \ - discord \ - https://discordpy.readthedocs.io/en/rewrite/ \ - https://discordpy.readthedocs.io/en/rewrite/objects.inv + python \ + https://docs.python.org/3/ \ + https://docs.python.org/3/objects.inv """ body = { 'package': package_name, diff --git a/bot/converters.py b/bot/converters.py index 339da7b60..6d6453486 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -49,7 +49,7 @@ class ValidURL(Converter): async with ctx.bot.http_session.get(url) as resp: if resp.status != 200: raise BadArgument( - f"HTTP GET on `{url}` returned status `{resp.status_code}`, expected 200" + f"HTTP GET on `{url}` returned status `{resp.status}`, expected 200" ) except CertificateError: if url.startswith('https'): -- cgit v1.2.3 From 607fcac8af43588f3e7dbe8bec2b3cdc15d19cad Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 1 Oct 2019 17:31:26 +0100 Subject: Make tests work with Union converter --- tests/cogs/test_information.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py index 986e73a65..184bd2595 100644 --- a/tests/cogs/test_information.py +++ b/tests/cogs/test_information.py @@ -9,6 +9,7 @@ from discord import ( CategoryChannel, Colour, Permissions, + Role, TextChannel, VoiceChannel, ) @@ -68,7 +69,7 @@ def test_roles_info_command(cog, ctx): def test_role_info_command(cog, ctx): - dummy_role = MagicMock() + dummy_role = MagicMock(spec=Role) dummy_role.name = "Dummy" dummy_role.colour = Colour.blurple() dummy_role.id = 112233445566778899 @@ -76,7 +77,7 @@ def test_role_info_command(cog, ctx): dummy_role.permissions = Permissions(0) dummy_role.members = [ctx.author] - admin_role = MagicMock() + admin_role = MagicMock(spec=Role) admin_role.name = "Admin" admin_role.colour = Colour.red() admin_role.id = 998877665544332211 @@ -89,7 +90,7 @@ def test_role_info_command(cog, ctx): cog.role_info.can_run = AsyncMock() cog.role_info.can_run.return_value = True - coroutine = cog.role_info.callback(cog, ctx, [dummy_role, admin_role]) + coroutine = cog.role_info.callback(cog, ctx, dummy_role, admin_role) assert asyncio.run(coroutine) is None -- cgit v1.2.3 From 83f890ec79167b047b73a93aacbb070111453196 Mon Sep 17 00:00:00 2001 From: bendiller Date: Tue, 1 Oct 2019 15:40:53 -0600 Subject: Add checks for valid response and retries to fetch_posts() --- bot/cogs/reddit.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 63a57c5c6..c626ad48c 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -21,6 +21,7 @@ class Reddit(Cog): HEADERS = {"User-Agent": "Discord Bot: PythonDiscord (https://pythondiscord.com/)"} URL = "https://www.reddit.com" + MAX_FETCH_RETRIES = 3 def __init__(self, bot: Bot): self.bot = bot @@ -42,16 +43,19 @@ class Reddit(Cog): if params is None: params = {} - response = await self.bot.http_session.get( - url=f"{self.URL}/{route}.json", - headers=self.HEADERS, - params=params - ) - - content = await response.json() - posts = content["data"]["children"] + for _ in range(self.MAX_FETCH_RETRIES): + response = await self.bot.http_session.get( + url=f"{self.URL}/{route}.json", + headers=self.HEADERS, + params=params + ) + if response.status == 200 and response.content_type == 'application/json': + # Got appropriate response - process and return. + content = await response.json() + posts = content["data"]["children"] + return posts[:amount] - return posts[:amount] + return list() # Failed to get appropriate response within allowed number of retries. async def send_top_posts( self, channel: TextChannel, subreddit: Subreddit, content: str = None, time: str = "all" -- cgit v1.2.3 From 514576666ff7856ca09e049cb90ee8ac993107e2 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 1 Oct 2019 23:41:09 +0200 Subject: Create !tools alias and split !site resources. This splits the former !site resources into two separate commands, one for !site resources and one for !site tools. This makes sense now that we've split up the lists into two pages. It also adds a new alias, !tools, to call this command. This addresses #478. https://github.com/python-discord/bot/issues/478 --- bot/cogs/alias.py | 8 ++++++++ bot/cogs/site.py | 27 ++++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 80ff37983..0df5498a1 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -53,6 +53,14 @@ class Alias (Cog): """Alias for invoking site resources.""" await self.invoke(ctx, "site resources") + @command(name="tools", hidden=True) + async def site_tools_alias(self, ctx): + """ + Alias for invoking site tools. + """ + + await self.invoke(ctx, "site tools") + @command(name="watch", hidden=True) async def bigbrother_watch_alias(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: """Alias for invoking bigbrother watch [user] [reason].""" diff --git a/bot/cogs/site.py b/bot/cogs/site.py index 4a423faa9..8a8fed575 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -44,17 +44,30 @@ class Site(Cog): async def site_resources(self, ctx: Context) -> None: """Info about the site's Resources page.""" learning_url = f"{PAGES_URL}/resources" - tools_url = f"{PAGES_URL}/tools" - embed = Embed(title="Resources & Tools") - embed.set_footer(text=f"{learning_url} | {tools_url}") + embed = Embed(title="Resources") + embed.set_footer(text=f"{learning_url}") embed.colour = Colour.blurple() embed.description = ( f"The [Resources page]({learning_url}) on our website contains a " - "list of hand-selected goodies that we regularly recommend " - f"to both beginners and experts. The [Tools page]({tools_url}) " - "contains a couple of the most popular tools for programming in " - "Python." + "list of hand-selected learning resources that we regularly recommend " + f"to both beginners and experts." + ) + + await ctx.send(embed=embed) + + @site_group.command(name="tools") + async def site_tools(self, ctx: Context): + """Info about the site's Tools page.""" + + tools_url = f"{PAGES_URL}/tools" + + embed = Embed(title="Tools") + embed.set_footer(text=f"{tools_url}") + embed.colour = Colour.blurple() + embed.description = ( + f"The [Tools page]({tools_url}) on our website contains a " + f"couple of the most popular tools for programming in Python." ) await ctx.send(embed=embed) -- cgit v1.2.3 From a994c2c2b67521b8a739536ae0c9d47de75ce7b8 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 1 Oct 2019 23:49:10 +0200 Subject: Fix missing type annotations and docstring errors. --- bot/cogs/alias.py | 7 ++----- bot/cogs/site.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 0df5498a1..0f49a400c 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -54,11 +54,8 @@ class Alias (Cog): await self.invoke(ctx, "site resources") @command(name="tools", hidden=True) - async def site_tools_alias(self, ctx): - """ - Alias for invoking site tools. - """ - + async def site_tools_alias(self, ctx: Context) -> None: + """Alias for invoking site tools.""" await self.invoke(ctx, "site tools") @command(name="watch", hidden=True) diff --git a/bot/cogs/site.py b/bot/cogs/site.py index 8a8fed575..c3bdf85e4 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -57,9 +57,8 @@ class Site(Cog): await ctx.send(embed=embed) @site_group.command(name="tools") - async def site_tools(self, ctx: Context): + async def site_tools(self, ctx: Context) -> None: """Info about the site's Tools page.""" - tools_url = f"{PAGES_URL}/tools" embed = Embed(title="Tools") -- cgit v1.2.3 From a70cf2070f6af0b7710b0934b7e812dce78330d0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 2 Oct 2019 06:44:21 +0200 Subject: Fix `cog_unload` bug in WatchChannel ABC https://github.com/python-discord/bot/issues/482 There was small bug in the `cog_unload` method of the WatchChannel ABC in `bot.cogs.watchchannels.watchchannel`. The problem was that it tries to check if the Task assigned to `self._consume_task` is done by accessing its `done` method. However, if a watch channel has not yet relayed messages after the bot has started, it will not have a consumption task yet, meaning this `_consume_task` attribute will be assigned to `None`. The solution is to change the `if` condition to: `if self._consume_task and not self._consume_task.done():` This commit closes #482 --- bot/cogs/watchchannels/watchchannel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index ce8014d69..760e012eb 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -335,7 +335,7 @@ class WatchChannel(metaclass=CogABCMeta): def cog_unload(self) -> None: """Takes care of unloading the cog and canceling the consumption task.""" self.log.trace(f"Unloading the cog") - if not self._consume_task.done(): + if self._consume_task and not self._consume_task.done(): self._consume_task.cancel() try: self._consume_task.result() -- cgit v1.2.3 From 5f81b80a4dea49195053ab0177f4fd9aa9bea5e5 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 2 Oct 2019 07:27:06 +0200 Subject: Apply docstring review suggestion Co-Authored-By: Mark --- tests/test_converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_converters.py b/tests/test_converters.py index aa692f9f8..8093f55ac 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -216,7 +216,7 @@ def test_isodatetime_converter_for_valid(datetime_string: str, expected_dt: date @pytest.mark.parametrize( ("datetime_string"), ( - # Make sure it doesn't interfere with the Duration converation + # Make sure it doesn't interfere with the Duration converter ('1Y'), ('1d'), ('1H'), -- cgit v1.2.3 From 333d5e69d03eab90ebfa8dcf53b48e8a04fe776f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 2 Oct 2019 07:27:46 +0200 Subject: Remove angle brackets from ISODateTime docstring This commit removes the angle brackets from the url in the docstring of `ISODateTime.convert`. The reason: it's ugly. --- bot/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index 0bd50c14c..59a6f6b07 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -229,7 +229,7 @@ class ISODateTime(Converter): `dateutil.parser`. In general, it accepts datetime strings that start with a date, optionally followed by a time. - See: + See: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse Formats that are guaranteed to be valid by our tests are: -- cgit v1.2.3 From a8b600217cb9ab4524bb307f0a6a922a0d8815be Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 2 Oct 2019 10:42:43 +0200 Subject: Make ISODateTime return tz-unaware datetime The parser we use, `dateutil.parsers.isoparse` returns a timezone- aware or timezone-unaware `datetime` object depending on whether or not the datetime string included a timezone offset specification. Since we can't compare tz-aware objects to tz-unaware objects it's better to make sure our converter is consistent in the type it will return. For now, I've chosen to return tz-unaware datetime objects, since `discord.py` also returns tz-unaware datetime objects when accessing datetime-related attributes of objects. Since we're likely to compare "our" datetime objects to discord.py-provided datetime objects, I think that's the most parsimonious option for now. Note: It's probably a good idea to open a larger discussion about using timezone-aware datetime objects throughout the library to avoid a UTC-time being interpreted as localtime. This will require a broader discussion than this commit/PR allows, though. --- bot/converters.py | 13 ++++++++++++- tests/test_converters.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index 59a6f6b07..27223e632 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -5,6 +5,7 @@ from ssl import CertificateError from typing import Union import dateutil.parser +import dateutil.tz import discord from aiohttp import ClientConnectorError from dateutil.relativedelta import relativedelta @@ -227,12 +228,18 @@ class ISODateTime(Converter): The converter is flexible in the formats it accepts, as it uses the `isoparse` method of `dateutil.parser`. In general, it accepts datetime strings that start with a date, - optionally followed by a time. + optionally followed by a time. Specifying a timezone offset in the datetime string is + supported, but the `datetime` object will be converted to UTC and will be returned without + `tzinfo` as a timezone-unaware `datetime` object. See: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse Formats that are guaranteed to be valid by our tests are: + - `YYYY-mm-ddTHH:MM:SSZ` | `YYYY-mm-dd HH:MM:SSZ` + - `YYYY-mm-ddTHH:MM:SS±HH:MM` | `YYYY-mm-dd HH:MM:SS±HH:MM` + - `YYYY-mm-ddTHH:MM:SS±HHMM` | `YYYY-mm-dd HH:MM:SS±HHMM` + - `YYYY-mm-ddTHH:MM:SS±HH` | `YYYY-mm-dd HH:MM:SS±HH` - `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS` - `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM` - `YYYY-mm-dd` @@ -247,4 +254,8 @@ class ISODateTime(Converter): except ValueError: raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string") + if dt.tzinfo: + dt = dt.astimezone(dateutil.tz.UTC) + dt = dt.replace(tzinfo=None) + return dt diff --git a/tests/test_converters.py b/tests/test_converters.py index 8093f55ac..86e8f2249 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -190,6 +190,27 @@ def test_duration_converter_for_invalid(duration: str): @pytest.mark.parametrize( ("datetime_string", "expected_dt"), ( + + # `YYYY-mm-ddTHH:MM:SSZ` | `YYYY-mm-dd HH:MM:SSZ` + ('2019-09-02T02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)), + + # `YYYY-mm-ddTHH:MM:SS±HH:MM` | `YYYY-mm-dd HH:MM:SS±HH:MM` + ('2019-09-02T03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02T00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), + + # `YYYY-mm-ddTHH:MM:SS±HHMM` | `YYYY-mm-dd HH:MM:SS±HHMM` + ('2019-09-02T03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02T00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02 00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), + + # `YYYY-mm-ddTHH:MM:SS±HH` | `YYYY-mm-dd HH:MM:SS±HH` + ('2019-09-02 03:03:05+01', datetime.datetime(2019, 9, 2, 2, 3, 5)), + ('2019-09-02T01:03:05-01', datetime.datetime(2019, 9, 2, 2, 3, 5)), + # `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS` ('2019-09-02T02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), ('2019-09-02 02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), -- cgit v1.2.3 From ce6a16d69291f92ca27ba0dfc83cb479f87bb384 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 3 Oct 2019 07:41:25 +1000 Subject: Fix mutes not being re-applied on rejoins. --- bot/cogs/moderation.py | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index b596f36e6..5aa873a47 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -80,6 +80,43 @@ class Moderation(Scheduler, Cog): if infraction["expires_at"] is not None: self.schedule_task(self.bot.loop, infraction["id"], infraction) + @Cog.listener() + async def on_member_join(self, member: Member) -> None: + """Reapply active mute infractions for returning members.""" + active_mutes = await self.bot.api_client.get( + 'bot/infractions', + params={'user__id': str(member.id), 'type': 'mute', 'active': 'true'} + ) + if not active_mutes: + return + + # assume a single mute because of restrictions elsewhere + mute = active_mutes[0] + + # transform expiration to delay in seconds + expiration_datetime = datetime.fromisoformat(mute["expires_at"][:-1]) + delay = expiration_datetime - datetime.utcnow() + delay_seconds = delay.total_seconds() + + # if under a minute or in the past + if delay_seconds < 60: + log.debug(f"Marking infraction {mute['id']} as inactive (expired).") + await self._deactivate_infraction(mute) + self.cancel_task(mute["id"]) + + # Notify the user that they've been unmuted. + await self.notify_pardon( + user=member, + title="You have been unmuted.", + content="You may now send messages in the server.", + icon_url=Icons.user_unmute + ) + return + + # allowing modlog since this is a passive action that should be logged + await member.add_roles(self._muted_role, reason=f"Re-applying active mute: {mute['id']}") + log.debug(f"User {member.id} has been re-muted on rejoin.") + # region: Permanent infractions @with_role(*MODERATION_ROLES) @@ -955,6 +992,11 @@ class Moderation(Scheduler, Cog): user_id = infraction_object["user"] infraction_type = infraction_object["type"] + await self.bot.api_client.patch( + 'bot/infractions/' + str(infraction_object['id']), + json={"active": False} + ) + if infraction_type == "mute": member: Member = guild.get_member(user_id) if member: @@ -970,11 +1012,6 @@ class Moderation(Scheduler, Cog): except NotFound: log.info(f"Tried to unban user `{user_id}`, but Discord does not have an active ban registered.") - await self.bot.api_client.patch( - 'bot/infractions/' + str(infraction_object['id']), - json={"active": False} - ) - def _infraction_to_string(self, infraction_object: Dict[str, Union[str, int, bool]]) -> str: """Convert the infraction object to a string representation.""" actor_id = infraction_object["actor"] -- cgit v1.2.3 From 2bc08cc669e886fe749590e75d4030a5dbcb3f71 Mon Sep 17 00:00:00 2001 From: bendiller Date: Wed, 2 Oct 2019 18:56:04 -0600 Subject: Add logging for invalid response (after all retries are exhausted) --- bot/cogs/reddit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index c626ad48c..5a5f43da9 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -43,9 +43,10 @@ class Reddit(Cog): if params is None: params = {} + url = f"{self.URL}/{route}.json" for _ in range(self.MAX_FETCH_RETRIES): response = await self.bot.http_session.get( - url=f"{self.URL}/{route}.json", + url=url, headers=self.HEADERS, params=params ) @@ -55,6 +56,7 @@ class Reddit(Cog): posts = content["data"]["children"] return posts[:amount] + log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}") return list() # Failed to get appropriate response within allowed number of retries. async def send_top_posts( -- cgit v1.2.3 From 0b59585bfd4a117dc1f3e6c680b20e37026a097e Mon Sep 17 00:00:00 2001 From: bendiller Date: Wed, 2 Oct 2019 19:13:13 -0600 Subject: Add sleep(3) between retries, with bot indicating typing during sleep --- bot/cogs/reddit.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 5a5f43da9..f072da354 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -34,7 +34,9 @@ class Reddit(Cog): self.new_posts_task = None self.top_weekly_posts_task = None - async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]: + async def fetch_posts( + self, channel: TextChannel, route: str, *, amount: int = 25, params: dict = None + ) -> List[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" # Reddit's JSON responses only provide 25 posts at most. if not 25 >= amount > 0: @@ -55,6 +57,8 @@ class Reddit(Cog): content = await response.json() posts = content["data"]["children"] return posts[:amount] + async with channel.typing(): + await asyncio.sleep(3) log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}") return list() # Failed to get appropriate response within allowed number of retries. @@ -69,6 +73,7 @@ class Reddit(Cog): # Get the posts posts = await self.fetch_posts( + channel=channel, route=f"{subreddit}/top", amount=5, params={ @@ -116,7 +121,7 @@ class Reddit(Cog): embed=embed ) - async def poll_new_posts(self) -> None: + async def poll_new_posts(self, channel: TextChannel) -> None: """Periodically search for new subreddit posts.""" while True: await asyncio.sleep(RedditConfig.request_delay) @@ -137,7 +142,7 @@ class Reddit(Cog): self.prev_lengths[subreddit] = content_length # Now we can actually fetch the new data - posts = await self.fetch_posts(f"{subreddit}/new") + posts = await self.fetch_posts(channel, f"{subreddit}/new") new_posts = [] # Only show new posts if we've checked before. @@ -266,7 +271,7 @@ class Reddit(Cog): if self.reddit_channel is not None: if self.new_posts_task is None: - self.new_posts_task = self.bot.loop.create_task(self.poll_new_posts()) + self.new_posts_task = self.bot.loop.create_task(self.poll_new_posts(self.reddit_channel)) if self.top_weekly_posts_task is None: self.top_weekly_posts_task = self.bot.loop.create_task(self.poll_top_weekly_posts()) else: -- cgit v1.2.3 From 629cb4d05405a155715da765a2408be9156eb215 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 3 Oct 2019 07:14:31 +0200 Subject: Check if tzinfo is None in ISODateTime test As we have decided that the converter should return naive datetime objects, we should explicitly test that datetime strings with a timezone offset are still converted to a naive datetime object. I have done this by adding a `tzinfo is None` assertion. --- tests/test_converters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_converters.py b/tests/test_converters.py index 86e8f2249..f69995ec6 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -231,7 +231,9 @@ def test_duration_converter_for_invalid(duration: str): ) def test_isodatetime_converter_for_valid(datetime_string: str, expected_dt: datetime.datetime): converter = ISODateTime() - assert asyncio.run(converter.convert(None, datetime_string)) == expected_dt + converted_dt = asyncio.run(converter.convert(None, datetime_string)) + assert converted_dt.tzinfo is None + assert converted_dt == expected_dt @pytest.mark.parametrize( -- cgit v1.2.3 From 241f74c7a51adc0e73e49a9e91ee9a3ae501a8ea Mon Sep 17 00:00:00 2001 From: bendiller Date: Thu, 3 Oct 2019 15:49:55 -0600 Subject: Move asyncio.sleep() to avoid disturbing function signatures. --- bot/cogs/reddit.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index f072da354..08a725900 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -34,9 +34,7 @@ class Reddit(Cog): self.new_posts_task = None self.top_weekly_posts_task = None - async def fetch_posts( - self, channel: TextChannel, route: str, *, amount: int = 25, params: dict = None - ) -> List[dict]: + async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" # Reddit's JSON responses only provide 25 posts at most. if not 25 >= amount > 0: @@ -57,8 +55,7 @@ class Reddit(Cog): content = await response.json() posts = content["data"]["children"] return posts[:amount] - async with channel.typing(): - await asyncio.sleep(3) + await asyncio.sleep(3) log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}") return list() # Failed to get appropriate response within allowed number of retries. @@ -72,14 +69,14 @@ class Reddit(Cog): embed.description = "" # Get the posts - posts = await self.fetch_posts( - channel=channel, - route=f"{subreddit}/top", - amount=5, - params={ - "t": time - } - ) + async with channel.typing(): + posts = await self.fetch_posts( + route=f"{subreddit}/top", + amount=5, + params={ + "t": time + } + ) if not posts: embed.title = random.choice(ERROR_REPLIES) @@ -121,7 +118,7 @@ class Reddit(Cog): embed=embed ) - async def poll_new_posts(self, channel: TextChannel) -> None: + async def poll_new_posts(self) -> None: """Periodically search for new subreddit posts.""" while True: await asyncio.sleep(RedditConfig.request_delay) @@ -142,7 +139,7 @@ class Reddit(Cog): self.prev_lengths[subreddit] = content_length # Now we can actually fetch the new data - posts = await self.fetch_posts(channel, f"{subreddit}/new") + posts = await self.fetch_posts(f"{subreddit}/new") new_posts = [] # Only show new posts if we've checked before. @@ -271,7 +268,7 @@ class Reddit(Cog): if self.reddit_channel is not None: if self.new_posts_task is None: - self.new_posts_task = self.bot.loop.create_task(self.poll_new_posts(self.reddit_channel)) + self.new_posts_task = self.bot.loop.create_task(self.poll_new_posts()) if self.top_weekly_posts_task is None: self.top_weekly_posts_task = self.bot.loop.create_task(self.poll_top_weekly_posts()) else: -- cgit v1.2.3 From df4906c9bd54dfd12201e3684cdfafb74693c06f Mon Sep 17 00:00:00 2001 From: Ben Diller Date: Thu, 3 Oct 2019 15:58:47 -0600 Subject: Improve readability Co-Authored-By: Mark --- bot/cogs/reddit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 08a725900..6880aab85 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -55,6 +55,7 @@ class Reddit(Cog): content = await response.json() posts = content["data"]["children"] return posts[:amount] + await asyncio.sleep(3) log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}") -- cgit v1.2.3 From 0d2868a6e170043c687bf4c3b29bbf3820035e97 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 4 Oct 2019 11:35:49 -0700 Subject: Show previous watch reason and total after invoking watch command --- bot/cogs/watchchannels/bigbrother.py | 22 +++++++++++++++++++++- bot/cogs/watchchannels/talentpool.py | 19 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index e191c2dbc..9a9178534 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -70,7 +70,27 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): if response is not None: self.watched_users[user.id] = response - await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to Big Brother.") + msg = f":white_check_mark: Messages sent by {user} will now be relayed to Big Brother." + + history = await self.bot.api_client.get( + self.api_endpoint, + params={ + "user__id": str(user.id), + "active": "false", + 'type': 'watch', + 'ordering': '-inserted_at' + } + ) + + if len(history) > 1: + total = f"({len(history) // 2} infractions total)" + end_reason = history[0]["reason"] + start_reason = f"Watched: {history[1]['reason']}" + msg += f"\n\nUser's previous watch reasons {total}:```{start_reason}\n\n{end_reason}```" + else: + msg = ":x: Failed to post the infraction: response was empty." + + await ctx.send(msg) @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 4a23902d5..ba4dea836 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -93,7 +93,24 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): resp.raise_for_status() self.watched_users[user.id] = response_data - await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to the talent pool channel") + msg = f":white_check_mark: Messages sent by {user} will now be relayed to the talent pool channel" + + history = await self.bot.api_client.get( + self.api_endpoint, + params={ + "user__id": str(user.id), + "active": "false", + "ordering": "-inserted_at" + } + ) + + if history: + total = f"({len(history)} nominations total)" + start_reason = f"Watched: {history[0]['reason']}" + end_reason = f"Unwatched: {history[0]['end_reason']}" + msg += f"\n\nUser's previous watch reasons {total}:```{start_reason}\n\n{end_reason}```" + + await ctx.send(msg) @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) -- cgit v1.2.3 From e353b00ca5e98f5587afcb36baba6ef7acb9f278 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 4 Oct 2019 12:41:05 -0700 Subject: Adjust verbiage of totals for watch commands --- bot/cogs/watchchannels/bigbrother.py | 2 +- bot/cogs/watchchannels/talentpool.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 9a9178534..3eba9862f 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -83,7 +83,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): ) if len(history) > 1: - total = f"({len(history) // 2} infractions total)" + total = f"({len(history) // 2} previous infractions in total)" end_reason = history[0]["reason"] start_reason = f"Watched: {history[1]['reason']}" msg += f"\n\nUser's previous watch reasons {total}:```{start_reason}\n\n{end_reason}```" diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index ba4dea836..176c6f760 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -105,7 +105,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) if history: - total = f"({len(history)} nominations total)" + total = f"({len(history)} previous nominations in total)" start_reason = f"Watched: {history[0]['reason']}" end_reason = f"Unwatched: {history[0]['end_reason']}" msg += f"\n\nUser's previous watch reasons {total}:```{start_reason}\n\n{end_reason}```" -- cgit v1.2.3