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 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 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 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 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 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 From a09ca83f9368a664f147071f85cb4b34489d0224 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 4 Oct 2019 13:33:16 -0700 Subject: Fix error when symbol_id cannot be found in doc HTML --- bot/cogs/doc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index c9e6b3b91..0c5a8fce3 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -207,6 +207,9 @@ class Doc(commands.Cog): symbol_heading = soup.find(id=symbol_id) signature_buffer = [] + if symbol_heading is None: + return None + # Traverse the tags of the signature header and ignore any # unwanted symbols from it. Add all of it to a temporary buffer. for tag in symbol_heading.strings: -- cgit v1.2.3 From 97bfd35e34b56336950aa36b103889e2606627e0 Mon Sep 17 00:00:00 2001 From: Derek Date: Sun, 6 Oct 2019 17:12:47 -0400 Subject: Update max threshold for attachments --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 38b26f64f..0dac9bf9f 100644 --- a/config-default.yml +++ b/config-default.yml @@ -282,7 +282,7 @@ anti_spam: rules: attachments: interval: 10 - max: 3 + max: 9 burst: interval: 10 -- cgit v1.2.3