From 37a604522d081e7dc668e70c9b5080d2d6df18eb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 13 Jun 2019 23:38:50 -0700 Subject: Banish RabbitMQ from this world --- bot/__main__.py | 12 --- bot/cogs/rmq.py | 229 ----------------------------------------- bot/cogs/snekbox.py | 18 +--- bot/constants.py | 9 -- bot/utils/service_discovery.py | 22 ---- config-default.yml | 7 -- 6 files changed, 1 insertion(+), 296 deletions(-) delete mode 100644 bot/cogs/rmq.py delete mode 100644 bot/utils/service_discovery.py diff --git a/bot/__main__.py b/bot/__main__.py index 8687cc62c..8afec2718 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -8,7 +8,6 @@ from discord.ext.commands import Bot, when_mentioned_or from bot.api import APIClient from bot.constants import Bot as BotConfig, DEBUG_MODE -from bot.utils.service_discovery import wait_for_rmq log = logging.getLogger(__name__) @@ -31,14 +30,6 @@ bot.http_session = ClientSession( ) bot.api_client = APIClient(loop=asyncio.get_event_loop()) -log.info("Waiting for RabbitMQ...") -has_rmq = wait_for_rmq() - -if has_rmq: - log.info("RabbitMQ found") -else: - log.warning("Timed out while waiting for RabbitMQ") - # Internal/debug bot.load_extension("bot.cogs.logging") bot.load_extension("bot.cogs.security") @@ -80,9 +71,6 @@ bot.load_extension("bot.cogs.token_remover") bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.wolfram") -if has_rmq: - bot.load_extension("bot.cogs.rmq") - bot.run(BotConfig.token) bot.http_session.close() # Close the aiohttp session when the bot finishes running diff --git a/bot/cogs/rmq.py b/bot/cogs/rmq.py deleted file mode 100644 index 2742fb969..000000000 --- a/bot/cogs/rmq.py +++ /dev/null @@ -1,229 +0,0 @@ -import asyncio -import datetime -import json -import logging -import pprint - -import aio_pika -from aio_pika import Message -from dateutil import parser as date_parser -from discord import Colour, Embed -from discord.ext.commands import Bot -from discord.utils import get - -from bot.constants import Channels, Guild, RabbitMQ - -log = logging.getLogger(__name__) - -LEVEL_COLOURS = { - "debug": Colour.blue(), - "info": Colour.green(), - "warning": Colour.gold(), - "error": Colour.red() -} - -DEFAULT_LEVEL_COLOUR = Colour.greyple() -EMBED_PARAMS = ( - "colour", "title", "url", "description", "timestamp" -) - -CONSUME_TIMEOUT = datetime.timedelta(seconds=10) - - -class RMQ: - """ - RabbitMQ event handling - """ - - rmq = None # type: aio_pika.Connection - channel = None # type: aio_pika.Channel - queue = None # type: aio_pika.Queue - - def __init__(self, bot: Bot): - self.bot = bot - - async def on_ready(self): - self.rmq = await aio_pika.connect_robust( - host=RabbitMQ.host, port=RabbitMQ.port, login=RabbitMQ.username, password=RabbitMQ.password - ) - - log.info("Connected to RabbitMQ") - - self.channel = await self.rmq.channel() - self.queue = await self.channel.declare_queue("bot_events", durable=True) - - log.debug("Channel opened, queue declared") - - async for message in self.queue: - with message.process(): - message.ack() - await self.handle_message(message, message.body.decode()) - - async def send_text(self, queue: str, data: str): - message = Message(data.encode("utf-8")) - await self.channel.default_exchange.publish(message, queue) - - async def send_json(self, queue: str, **data): - message = Message(json.dumps(data).encode("utf-8")) - await self.channel.default_exchange.publish(message, queue) - - async def consume(self, queue: str, **kwargs): - queue_obj = await self.channel.declare_queue(queue, **kwargs) - - result = None - start_time = datetime.datetime.now() - - while result is None: - if datetime.datetime.now() - start_time >= CONSUME_TIMEOUT: - result = "Timed out while waiting for a response." - else: - result = await queue_obj.get(timeout=5, fail=False) - await asyncio.sleep(0.5) - - if result: - result.ack() - - return result - - async def handle_message(self, message, data): - log.debug(f"Message: {message}") - log.debug(f"Data: {data}") - - try: - data = json.loads(data) - except Exception: - await self.do_mod_log("error", "Unable to parse event", data) - else: - event = data["event"] - event_data = data["data"] - - try: - func = getattr(self, f"do_{event}") - await func(**event_data) - except Exception as e: - await self.do_mod_log( - "error", f"Unable to handle event: {event}", - str(e) - ) - - async def do_mod_log(self, level: str, title: str, message: str): - colour = LEVEL_COLOURS.get(level, DEFAULT_LEVEL_COLOUR) - embed = Embed( - title=title, description=f"```\n{message}\n```", - colour=colour, timestamp=datetime.datetime.now() - ) - - await self.bot.get_channel(Channels.modlog).send(embed=embed) - log.log(logging._nameToLevel[level.upper()], f"Modlog: {title} | {message}") - - async def do_send_message(self, target: int, message: str): - channel = self.bot.get_channel(target) - - if channel is None: - await self.do_mod_log( - "error", "Failed: Send Message", - f"Unable to find channel: {target}" - ) - else: - await channel.send(message) - - await self.do_mod_log( - "info", "Succeeded: Send Embed", - f"Message sent to channel {target}\n\n{message}" - ) - - async def do_send_embed(self, target: int, **embed_params): - for param, value in list(embed_params.items()): # To keep a full copy - if param not in EMBED_PARAMS: - await self.do_mod_log( - "warning", "Warning: Send Embed", - f"Unknown embed parameter: {param}" - ) - del embed_params[param] - - if param == "timestamp": - embed_params[param] = date_parser.parse(value) - elif param == "colour": - embed_params[param] = Colour(value) - - channel = self.bot.get_channel(target) - - if channel is None: - await self.do_mod_log( - "error", "Failed: Send Embed", - f"Unable to find channel: {target}" - ) - else: - await channel.send(embed=Embed(**embed_params)) - - await self.do_mod_log( - "info", "Succeeded: Send Embed", - f"Embed sent to channel {target}\n\n{pprint.pformat(embed_params, 4)}" - ) - - async def do_add_role(self, target: int, role_id: int, reason: str): - guild = self.bot.get_guild(Guild.id) - member = guild.get_member(int(target)) - - if member is None: - return await self.do_mod_log( - "error", "Failed: Add Role", - f"Unable to find member: {target}" - ) - - role = get(guild.roles, id=int(role_id)) - - if role is None: - return await self.do_mod_log( - "error", "Failed: Add Role", - f"Unable to find role: {role_id}" - ) - - try: - await member.add_roles(role, reason=reason) - except Exception as e: - await self.do_mod_log( - "error", "Failed: Add Role", - f"Error while adding role {role.name}: {e}" - ) - else: - await self.do_mod_log( - "info", "Succeeded: Add Role", - f"Role {role.name} added to member {target}" - ) - - async def do_remove_role(self, target: int, role_id: int, reason: str): - guild = self.bot.get_guild(Guild.id) - member = guild.get_member(int(target)) - - if member is None: - return await self.do_mod_log( - "error", "Failed: Remove Role", - f"Unable to find member: {target}" - ) - - role = get(guild.roles, id=int(role_id)) - - if role is None: - return await self.do_mod_log( - "error", "Failed: Remove Role", - f"Unable to find role: {role_id}" - ) - - try: - await member.remove_roles(role, reason=reason) - except Exception as e: - await self.do_mod_log( - "error", "Failed: Remove Role", - f"Error while adding role {role.name}: {e}" - ) - else: - await self.do_mod_log( - "info", "Succeeded: Remove Role", - f"Role {role.name} removed from member {target}" - ) - - -def setup(bot): - bot.add_cog(RMQ(bot)) - log.info("Cog loaded: RMQ") diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index cb0454249..1988303f1 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -9,7 +9,6 @@ from discord.ext.commands import ( Bot, CommandError, Context, NoPrivateMessage, command, guild_only ) -from bot.cogs.rmq import RMQ from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, Roles, URLs from bot.decorators import InChannelCheckFailure, in_channel from bot.utils.messages import wait_for_deletion @@ -17,12 +16,6 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) -RMQ_ARGS = { - "durable": False, - "arguments": {"x-message-ttl": 5000}, - "auto_delete": True -} - CODE_TEMPLATE = """ venv_file = "/snekbox/.venv/bin/activate_this.py" exec(open(venv_file).read(), dict(__file__=venv_file)) @@ -64,10 +57,6 @@ class Snekbox: self.bot = bot self.jobs = {} - @property - def rmq(self) -> RMQ: - return self.bot.get_cog("RMQ") - @command(name='eval', aliases=('e',)) @guild_only() @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) @@ -107,13 +96,8 @@ class Snekbox: code = CODE_TEMPLATE.replace("{CODE}", code) try: - await self.rmq.send_json( - "input", - snekid=str(ctx.author.id), message=code - ) - async with ctx.typing(): - message = await self.rmq.consume(str(ctx.author.id), **RMQ_ARGS) + message = ... # TODO paste_link = None if isinstance(message, str): diff --git a/bot/constants.py b/bot/constants.py index d2e3bb315..3ceecab85 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -374,15 +374,6 @@ class Keys(metaclass=YAMLGetter): youtube: str -class RabbitMQ(metaclass=YAMLGetter): - section = "rabbitmq" - - host: str - password: str - port: int - username: str - - class URLs(metaclass=YAMLGetter): section = "urls" diff --git a/bot/utils/service_discovery.py b/bot/utils/service_discovery.py deleted file mode 100644 index 8d79096bd..000000000 --- a/bot/utils/service_discovery.py +++ /dev/null @@ -1,22 +0,0 @@ -import datetime -import socket -import time -from contextlib import closing - -from bot.constants import RabbitMQ - -THIRTY_SECONDS = datetime.timedelta(seconds=30) - - -def wait_for_rmq(): - start = datetime.datetime.now() - - while True: - if datetime.datetime.now() - start > THIRTY_SECONDS: - return False - - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - if sock.connect_ex((RabbitMQ.host, RabbitMQ.port)) == 0: - return True - - time.sleep(0.5) diff --git a/config-default.yml b/config-default.yml index f6481cfcd..8d9d0f999 100644 --- a/config-default.yml +++ b/config-default.yml @@ -204,13 +204,6 @@ keys: youtube: !ENV "YOUTUBE_API_KEY" -rabbitmq: - host: "pdrmq" - password: !ENV ["RABBITMQ_DEFAULT_PASS", "guest"] - port: 5672 - username: !ENV ["RABBITMQ_DEFAULT_USER", "guest"] - - urls: # PyDis site vars site: &DOMAIN "pythondiscord.com" -- cgit v1.2.3 From 6aaff26efcf9132ed4a3f35e5f2ac4516441027b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 13 Jun 2019 23:39:22 -0700 Subject: Add snekbox endpoint to constants --- bot/constants.py | 3 +++ config-default.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 3ceecab85..0bd950a7d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -377,6 +377,9 @@ class Keys(metaclass=YAMLGetter): class URLs(metaclass=YAMLGetter): section = "urls" + # Snekbox endpoints + snekbox_eval_api: str + # Discord API endpoints discord_api: str discord_invite_api: str diff --git a/config-default.yml b/config-default.yml index 8d9d0f999..7854b5db9 100644 --- a/config-default.yml +++ b/config-default.yml @@ -235,6 +235,9 @@ urls: site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] + # Snekbox + snekbox_eval_api: "http://localhost:8060/eval" + # Env vars deploy: !ENV "DEPLOY_URL" status: !ENV "STATUS_URL" -- cgit v1.2.3 From 50bbe09dfbfb6f5300dfce7e2edad2bfd9819dbc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 09:56:12 -0700 Subject: Snekbox: add a function to send a request to the API --- bot/cogs/snekbox.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 1988303f1..70a19db87 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -57,6 +57,13 @@ class Snekbox: self.bot = bot self.jobs = {} + async def post_eval(self, code: str) -> dict: + """Send a POST request to the Snekbox API to evaluate code and return the results.""" + url = URLs.snekbox_eval_api + data = {"input": code} + async with self.bot.http_session.post(url, json=data, raise_for_status=True) as resp: + return await resp.json() + @command(name='eval', aliases=('e',)) @guild_only() @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) -- cgit v1.2.3 From 1f9d9eb3072a0f63044efc7846a474339a69f219 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 10:11:43 -0700 Subject: Snekbox: move input code preparation to a separate function --- bot/cogs/snekbox.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 70a19db87..1d18d2d84 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -64,6 +64,28 @@ class Snekbox: async with self.bot.http_session.post(url, json=data, raise_for_status=True) as resp: return await resp.json() + @staticmethod + def prepare_input(code: str) -> str: + """Extract code from the Markdown, format it, and insert it into the code template.""" + match = FORMATTED_CODE_REGEX.fullmatch(code) + if match: + code, block, lang, delim = match.group("code", "block", "lang", "delim") + code = textwrap.dedent(code) + if block: + info = (f"'{lang}' highlighted" if lang else "plain") + " code block" + else: + info = f"{delim}-enclosed inline code" + log.trace(f"Extracted {info} for evaluation:\n{code}") + else: + code = textwrap.dedent(RAW_CODE_REGEX.fullmatch(code).group("code")) + log.trace( + f"Eval message contains unformatted or badly formatted code, " + f"stripping whitespace only:\n{code}" + ) + + code = textwrap.indent(code, " ") + return CODE_TEMPLATE.replace("{CODE}", code) + @command(name='eval', aliases=('e',)) @guild_only() @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) @@ -85,22 +107,7 @@ class Snekbox: log.info(f"Received code from {ctx.author.name}#{ctx.author.discriminator} for evaluation:\n{code}") self.jobs[ctx.author.id] = datetime.datetime.now() - # Strip whitespace and inline or block code markdown and extract the code and some formatting info - match = FORMATTED_CODE_REGEX.fullmatch(code) - if match: - code, block, lang, delim = match.group("code", "block", "lang", "delim") - code = textwrap.dedent(code) - if block: - info = (f"'{lang}' highlighted" if lang else "plain") + " code block" - else: - info = f"{delim}-enclosed inline code" - log.trace(f"Extracted {info} for evaluation:\n{code}") - else: - code = textwrap.dedent(RAW_CODE_REGEX.fullmatch(code).group("code")) - log.trace(f"Eval message contains not or badly formatted code, stripping whitespace only:\n{code}") - - code = textwrap.indent(code, " ") - code = CODE_TEMPLATE.replace("{CODE}", code) + code = self.prepare_input(code) try: async with ctx.typing(): -- cgit v1.2.3 From e003dfc9e9fa2bb876a71f9b73a764d0f4bc6186 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 10:46:04 -0700 Subject: Snekbox: move paste service upload to a separate function --- bot/cogs/snekbox.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 1d18d2d84..9b0c39ac1 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -3,6 +3,7 @@ import logging import random import re import textwrap +from typing import Optional from discord import Colour, Embed from discord.ext.commands import ( @@ -64,6 +65,18 @@ class Snekbox: async with self.bot.http_session.post(url, json=data, raise_for_status=True) as resp: return await resp.json() + async def upload_output(self, output: str) -> Optional[str]: + """Upload the eval output to a paste service and return a URL to it if successful.""" + url = URLs.paste_service.format(key="documents") + try: + async with self.bot.http_session.post(url, data=output, raise_for_status=True) as resp: + data = await resp.json() + + if "key" in data: + return URLs.paste_service.format(key=data["key"]) + except Exception: + log.exception("Failed to upload full output to paste service!") + @staticmethod def prepare_input(code: str) -> str: """Extract code from the Markdown, format it, and insert it into the code template.""" @@ -149,16 +162,7 @@ class Snekbox: truncated = True if truncated: - try: - response = await self.bot.http_session.post( - URLs.paste_service.format(key="documents"), - data=full_output - ) - data = await response.json() - if "key" in data: - paste_link = URLs.paste_service.format(key=data["key"]) - except Exception: - log.exception("Failed to upload full output to paste service!") + paste_link = await self.upload_output(full_output) if output.strip(): if paste_link: -- cgit v1.2.3 From 07d1126867256cd84844b8da883e4e837a60f9f8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 11:05:35 -0700 Subject: Snekbox: move output formatting to a separate function --- bot/cogs/snekbox.py | 87 +++++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 9b0c39ac1..cb31d8ccd 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -3,7 +3,7 @@ import logging import random import re import textwrap -from typing import Optional +from typing import Optional, Tuple from discord import Colour, Embed from discord.ext.commands import ( @@ -99,6 +99,50 @@ class Snekbox: code = textwrap.indent(code, " ") return CODE_TEMPLATE.replace("{CODE}", code) + async def format_output(self, output: str) -> Tuple[str, Optional[str]]: + """ + Format the output and return a tuple of the formatted output and a URL to the full output. + + Prepend each line with a line number. Truncate if there are over 10 lines or 1000 characters + and upload the full output to a paste service. + """ + output = output.strip(" \n") + paste_link = None + + if "<@" in output: + output = output.replace("<@", "<@\u200B") # Zero-width space + + if " 0: + output = [f"{i:03d} | {line}" for i, line in enumerate(output.split("\n"), start=1)] + output = "\n".join(output) + + if output.count("\n") > 10: + output = "\n".join(output.split("\n")[:10]) + + if len(output) >= 1000: + output = f"{output[:1000]}\n... (truncated - too long, too many lines)" + else: + output = f"{output}\n... (truncated - too many lines)" + truncated = True + + elif len(output) >= 1000: + output = f"{output[:1000]}\n... (truncated - too long)" + truncated = True + + if truncated: + paste_link = await self.upload_output(full_output) + + return output.strip(), paste_link + @command(name='eval', aliases=('e',)) @guild_only() @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) @@ -125,46 +169,9 @@ class Snekbox: try: async with ctx.typing(): message = ... # TODO - paste_link = None + output, paste_link = await self.format_output(message) - if isinstance(message, str): - output = str.strip(" \n") - else: - output = message.body.decode().strip(" \n") - - if "<@" in output: - output = output.replace("<@", "<@\u200B") # Zero-width space - - if " 0: - output = [f"{i:03d} | {line}" for i, line in enumerate(output.split("\n"), start=1)] - output = "\n".join(output) - - if output.count("\n") > 10: - output = "\n".join(output.split("\n")[:10]) - - if len(output) >= 1000: - output = f"{output[:1000]}\n... (truncated - too long, too many lines)" - else: - output = f"{output}\n... (truncated - too many lines)" - truncated = True - - elif len(output) >= 1000: - output = f"{output[:1000]}\n... (truncated - too long)" - truncated = True - - if truncated: - paste_link = await self.upload_output(full_output) - - if output.strip(): + if output: if paste_link: msg = f"{ctx.author.mention} Your eval job has completed.\n\n```py\n{output}\n```" \ f"\nFull output: {paste_link}" -- cgit v1.2.3 From 0024395c9b081eb4898352fb3f0bd390d72b0976 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 13:04:27 -0700 Subject: Snekbox: refactor eval command --- bot/cogs/snekbox.py | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index cb31d8ccd..f6cf31487 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -148,48 +148,48 @@ class Snekbox: @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) async def eval_command(self, ctx: Context, *, code: str = None): """ - Run some code. get the result back. We've done our best to make this safe, but do let us know if you - manage to find an issue with it! + Run Python code and get the results. - This command supports multiple lines of code, including code wrapped inside a formatted code block. + This command supports multiple lines of code, including code wrapped inside a formatted code + block. We've done our best to make this safe, but do let us know if you manage to find an + issue with it! """ - if ctx.author.id in self.jobs: - await ctx.send(f"{ctx.author.mention} You've already got a job running - please wait for it to finish!") - return + return await ctx.send( + f"{ctx.author.mention} You've already got a job running - " + f"please wait for it to finish!" + ) if not code: # None or empty string return await ctx.invoke(self.bot.get_command("help"), "eval") - log.info(f"Received code from {ctx.author.name}#{ctx.author.discriminator} for evaluation:\n{code}") - self.jobs[ctx.author.id] = datetime.datetime.now() + log.info( + f"Received code from {ctx.author.name}#{ctx.author.discriminator} " + f"for evaluation:\n{code}" + ) + self.jobs[ctx.author.id] = datetime.datetime.now() code = self.prepare_input(code) try: async with ctx.typing(): - message = ... # TODO - output, paste_link = await self.format_output(message) + results = await self.post_eval(code) + output, paste_link = await self.format_output(results["stdout"]) - if output: - if paste_link: - msg = f"{ctx.author.mention} Your eval job has completed.\n\n```py\n{output}\n```" \ - f"\nFull output: {paste_link}" - else: - msg = f"{ctx.author.mention} Your eval job has completed.\n\n```py\n{output}\n```" + if not output: + output = "[No output]" - response = await ctx.send(msg) - self.bot.loop.create_task(wait_for_deletion(response, user_ids=(ctx.author.id,), client=ctx.bot)) + msg = f"{ctx.author.mention} Your eval job has completed.\n\n```py\n{output}\n```" - else: - await ctx.send( - f"{ctx.author.mention} Your eval job has completed.\n\n```py\n[No output]\n```" - ) + if paste_link: + msg = f"{msg}\nFull output: {paste_link}" + response = await ctx.send(msg) + self.bot.loop.create_task( + wait_for_deletion(response, user_ids=(ctx.author.id,), client=ctx.bot) + ) + finally: del self.jobs[ctx.author.id] - except Exception: - del self.jobs[ctx.author.id] - raise @eval_command.error async def eval_command_error(self, ctx: Context, error: CommandError): -- cgit v1.2.3 From 6fe67047ba4df4949cf5f86aaa7df19d90788601 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 19:12:35 -0700 Subject: Snekbox: redirect stderr to stdout * Save original output before processing output --- bot/cogs/snekbox.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index f6cf31487..909479f4f 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -21,7 +21,11 @@ CODE_TEMPLATE = """ venv_file = "/snekbox/.venv/bin/activate_this.py" exec(open(venv_file).read(), dict(__file__=venv_file)) +import contextlib +import sys + try: + with contextlib.redirect_stderr(sys.stdout): {CODE} except Exception as e: print(e) @@ -75,6 +79,7 @@ class Snekbox: if "key" in data: return URLs.paste_service.format(key=data["key"]) except Exception: + # 400 (Bad Request) means the data is too large log.exception("Failed to upload full output to paste service!") @staticmethod @@ -96,7 +101,7 @@ class Snekbox: f"stripping whitespace only:\n{code}" ) - code = textwrap.indent(code, " ") + code = textwrap.indent(code, " ") return CODE_TEMPLATE.replace("{CODE}", code) async def format_output(self, output: str) -> Tuple[str, Optional[str]]: @@ -107,6 +112,7 @@ class Snekbox: and upload the full output to a paste service. """ output = output.strip(" \n") + original_output = output # To be uploaded to a pasting service if needed paste_link = None if "<@" in output: @@ -118,8 +124,6 @@ class Snekbox: if ESCAPE_REGEX.findall(output): return "Code block escape attempt detected; will not output result", paste_link - # the original output, to send to a pasting service if needed - full_output = output truncated = False if output.count("\n") > 0: output = [f"{i:03d} | {line}" for i, line in enumerate(output.split("\n"), start=1)] @@ -139,7 +143,7 @@ class Snekbox: truncated = True if truncated: - paste_link = await self.upload_output(full_output) + paste_link = await self.upload_output(original_output) return output.strip(), paste_link -- cgit v1.2.3 From acee1bfb6ecb1df0f21f06d1444dd2d7c06f9819 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 20:33:12 -0700 Subject: Snekbox: provide more descriptive messages for failures Uses the process's return code to determine an appropriate message. Furthermore, the signal's name is returned if applicable. * Log process's return code * Edit cog docstring --- bot/cogs/snekbox.py | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 909479f4f..a01348f3a 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -3,6 +3,7 @@ import logging import random import re import textwrap +from signal import Signals from typing import Optional, Tuple from discord import Colour, Embed @@ -55,7 +56,7 @@ BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) class Snekbox: """ - Safe evaluation using Snekbox + Safe evaluation of Python code using Snekbox """ def __init__(self, bot: Bot): @@ -104,6 +105,31 @@ class Snekbox: code = textwrap.indent(code, " ") return CODE_TEMPLATE.replace("{CODE}", code) + @staticmethod + def get_results_message(results: dict) -> Tuple[str, str]: + """Return a user-friendly message and error corresponding to the process's return code.""" + stderr, returncode = results["stderr"], results["returncode"] + msg = f"Your eval job has completed with return code {returncode}" + error = "" + + if returncode is None: + msg = "Your eval job has failed" + error = stderr.strip() + elif returncode == 128 + Signals.SIGKILL: + msg = "Your eval job timed out or ran out of memory" + elif returncode == 255: + msg = "Your eval job has failed" + error = "A fatal NsJail error occurred" + else: + # Try to append signal's name if one exists + try: + name = Signals(returncode - 128).name + msg = f"{msg} ({name})" + except ValueError: + pass + + return msg, error + async def format_output(self, output: str) -> Tuple[str, Optional[str]]: """ Format the output and return a tuple of the formatted output and a URL to the full output. @@ -145,9 +171,13 @@ class Snekbox: if truncated: paste_link = await self.upload_output(original_output) - return output.strip(), paste_link + output = output.strip() + if not output: + output = "[No output]" - @command(name='eval', aliases=('e',)) + return output, paste_link + + @command(name="eval", aliases=("e",)) @guild_only() @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) async def eval_command(self, ctx: Context, *, code: str = None): @@ -178,13 +208,14 @@ class Snekbox: try: async with ctx.typing(): results = await self.post_eval(code) - output, paste_link = await self.format_output(results["stdout"]) - - if not output: - output = "[No output]" + msg, error = self.get_results_message(results) - msg = f"{ctx.author.mention} Your eval job has completed.\n\n```py\n{output}\n```" + if error: + output, paste_link = error, None + else: + output, paste_link = await self.format_output(results["stdout"]) + msg = f"{ctx.author.mention} {msg}.\n\n```py\n{output}\n```" if paste_link: msg = f"{msg}\nFull output: {paste_link}" @@ -192,6 +223,11 @@ class Snekbox: self.bot.loop.create_task( wait_for_deletion(response, user_ids=(ctx.author.id,), client=ctx.bot) ) + + log.info( + f"{ctx.author.name}#{ctx.author.discriminator}'s job had a return code of " + f"{results['returncode']}" + ) finally: del self.jobs[ctx.author.id] -- cgit v1.2.3 From d13802b1572220e33c13d7e67aef0039aa8d0293 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 14 Jun 2019 20:53:14 -0700 Subject: Snekbox: make output formatting more efficient * Only prepend line numbers to the first 10 lines * Use generator expression for prepending line numbers to output * Add trace logging --- bot/cogs/snekbox.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index a01348f3a..64e926257 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -72,6 +72,8 @@ class Snekbox: async def upload_output(self, output: str) -> Optional[str]: """Upload the eval output to a paste service and return a URL to it if successful.""" + log.trace("Uploading full output to paste service...") + url = URLs.paste_service.format(key="documents") try: async with self.bot.http_session.post(url, data=output, raise_for_status=True) as resp: @@ -137,6 +139,8 @@ class Snekbox: Prepend each line with a line number. Truncate if there are over 10 lines or 1000 characters and upload the full output to a paste service. """ + log.trace("Formatting output...") + output = output.strip(" \n") original_output = output # To be uploaded to a pasting service if needed paste_link = None @@ -151,22 +155,22 @@ class Snekbox: return "Code block escape attempt detected; will not output result", paste_link truncated = False - if output.count("\n") > 0: - output = [f"{i:03d} | {line}" for i, line in enumerate(output.split("\n"), start=1)] - output = "\n".join(output) + lines = output.count("\n") - if output.count("\n") > 10: - output = "\n".join(output.split("\n")[:10]) + if lines > 0: + output = output.split("\n")[:10] # Only first 10 cause the rest is truncated anyway + output = (f"{i:03d} | {line}" for i, line in enumerate(output, 1)) + output = "\n".join(output) + if lines > 10: + truncated = True if len(output) >= 1000: output = f"{output[:1000]}\n... (truncated - too long, too many lines)" else: output = f"{output}\n... (truncated - too many lines)" - truncated = True - elif len(output) >= 1000: - output = f"{output[:1000]}\n... (truncated - too long)" truncated = True + output = f"{output[:1000]}\n... (truncated - too long)" if truncated: paste_link = await self.upload_output(original_output) -- cgit v1.2.3 From 82be5b29c9e849aaf43b99a4ed3fd829289b12dc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 15 Jun 2019 18:45:51 -0700 Subject: Snekbox: adjust for API change that merged stderr into stdout --- bot/cogs/snekbox.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 64e926257..fd30aebcb 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -22,11 +22,7 @@ CODE_TEMPLATE = """ venv_file = "/snekbox/.venv/bin/activate_this.py" exec(open(venv_file).read(), dict(__file__=venv_file)) -import contextlib -import sys - try: - with contextlib.redirect_stderr(sys.stdout): {CODE} except Exception as e: print(e) @@ -104,19 +100,19 @@ class Snekbox: f"stripping whitespace only:\n{code}" ) - code = textwrap.indent(code, " ") + code = textwrap.indent(code, " ") return CODE_TEMPLATE.replace("{CODE}", code) @staticmethod def get_results_message(results: dict) -> Tuple[str, str]: """Return a user-friendly message and error corresponding to the process's return code.""" - stderr, returncode = results["stderr"], results["returncode"] + stdout, returncode = results["stdout"], results["returncode"] msg = f"Your eval job has completed with return code {returncode}" error = "" if returncode is None: msg = "Your eval job has failed" - error = stderr.strip() + error = stdout.strip() elif returncode == 128 + Signals.SIGKILL: msg = "Your eval job timed out or ran out of memory" elif returncode == 255: -- cgit v1.2.3 From ae959848e1332266464e824aa58a7c41fdce8312 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 15 Jun 2019 19:20:43 -0700 Subject: Snekbox: remove code template Removing the template allows for line numbers in tracebacks to correspond to the input code. Since stderr has been merged into stdout, exceptions will already be captured. Thus, it is redundant to wrap the code in a try-except. Importing of the site module has been re-enabled on Snekbox which automatically adds site-packages to sys.path. Thus, the virtual environment does not need to be activated anymore in a template. --- bot/cogs/snekbox.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index fd30aebcb..e10c6c2aa 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -18,16 +18,6 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) -CODE_TEMPLATE = """ -venv_file = "/snekbox/.venv/bin/activate_this.py" -exec(open(venv_file).read(), dict(__file__=venv_file)) - -try: -{CODE} -except Exception as e: - print(e) -""" - ESCAPE_REGEX = re.compile("[`\u202E\u200B]{3,}") FORMATTED_CODE_REGEX = re.compile( r"^\s*" # any leading whitespace from the beginning of the string @@ -100,8 +90,7 @@ class Snekbox: f"stripping whitespace only:\n{code}" ) - code = textwrap.indent(code, " ") - return CODE_TEMPLATE.replace("{CODE}", code) + return code @staticmethod def get_results_message(results: dict) -> Tuple[str, str]: -- cgit v1.2.3 From 6dccbd38016b113983b195f15272a5f432df6dcb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Jun 2019 13:54:53 -0700 Subject: Snekbox: limit paste service uploads to 1000 characters --- bot/cogs/snekbox.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index e10c6c2aa..0f8d3e4b6 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -38,6 +38,7 @@ RAW_CODE_REGEX = re.compile( ) BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) +MAX_PASTE_LEN = 1000 class Snekbox: @@ -60,6 +61,10 @@ class Snekbox: """Upload the eval output to a paste service and return a URL to it if successful.""" log.trace("Uploading full output to paste service...") + if len(output) > MAX_PASTE_LEN: + log.info("Full output is too long to upload") + return "too long to upload" + url = URLs.paste_service.format(key="documents") try: async with self.bot.http_session.post(url, data=output, raise_for_status=True) as resp: @@ -68,7 +73,7 @@ class Snekbox: if "key" in data: return URLs.paste_service.format(key=data["key"]) except Exception: - # 400 (Bad Request) means the data is too large + # 400 (Bad Request) means there are too many characters log.exception("Failed to upload full output to paste service!") @staticmethod -- cgit v1.2.3 From 54314b77a0687fd3d93ef9663ecd0555cc0f2b7b Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 23 Apr 2019 16:58:21 +0200 Subject: Adding the WatchChannel ABC --- bot/cogs/watchchannels/watchchannel.py | 341 +++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 bot/cogs/watchchannels/watchchannel.py diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py new file mode 100644 index 000000000..856211139 --- /dev/null +++ b/bot/cogs/watchchannels/watchchannel.py @@ -0,0 +1,341 @@ +import asyncio +import datetime +import logging +import re +from abc import ABC, abstractmethod +from collections import defaultdict, deque +from typing import Optional + +import aiohttp +import discord +from discord import Color, Embed, Message, errors +from discord.ext.commands import Bot, Context + +from bot.constants import ( + BigBrother as BigBrotherConfig, Guild as GuildConfig +) +from bot.pagination import LinePaginator +from bot.utils import messages +from bot.utils.time import time_since + +log = logging.getLogger(__name__) + +URL_RE = re.compile(r"(https?://[^\s]+)") + + +class WatchChannel(ABC): + """ + Base class for WatchChannels + + Abstracts the basic functionality for watchchannels in + a granular manner to allow for easy overwritting of + methods in the child class. + """ + + @abstractmethod + def __init__(self, bot: Bot) -> None: + """ + abstractmethod for __init__ which should still be called with super(). + + Note: Some of the attributes below need to be overwritten in the + __init__ of the child after the super().__init__(*args, **kwargs) + call. + """ + + self.bot = bot + + # These attributes need to be overwritten in the child class + self.destination = None # Channels.big_brother_logs + self.webhook_id = None # Some t.b.d. constant + self.api_endpoint = None # 'bot/infractions' + self.api_default_params = None # {'active': 'true', 'type': 'watch'} + + # These attributes can be left as they are in the child class + self._consume_task = None + self.watched_users = defaultdict(dict) + self.message_queue = defaultdict(lambda: defaultdict(deque)) + self.consumption_queue = None + self.retries = 5 + self.retry_delay = 10 + self.channel = None + self.webhook = None + self.message_history = [None, None, 0] + + self._start = self.bot.loop.create_task(self.start_watchchannel()) + + @property + def consuming_messages(self) -> bool: + """Checks if a consumption task is currently running.""" + + if self._consume_task is None: + return False + + if self._consume_task.done(): + exc = self._consume_task.exception() + if exc: + log.exception( + f"{self.__class__.__name__} consume task has failed with:", + exc_info=exc + ) + return False + + return True + + # @Cog.listener() + async def start_watchchannel(self) -> None: + """Retrieves watched users from the API.""" + + await self.bot.wait_until_ready() + + if await self.initialize_channel() and await self.fetch_user_cache(): + log.trace(f"Started the {self.__class__.__name__} WatchChannel") + else: + log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") + + async def initialize_channel(self) -> bool: + """ + Checks if channel and webhook are set; if not, tries to initialize them. + + Since the internal channel cache may not be available directly after `ready`, + this function will retry to get the channel a number of times. If both the + channel and webhook were initialized successfully. this function will return + `True`. + """ + + if self.channel is None: + for attempt in range(1, self.retries + 1): + self.channel = self.bot.get_channel(self.destination) + + if self.channel is None: + log.error(f"Failed to get the {self.__class__.__name__} channel; cannot watch users") + if attempt < self.initialization_retries: + log.error( + f"Attempt {attempt}/{self.retries}; " + f"Retrying in {self.retry_delay} seconds...") + await asyncio.sleep(self.retry_delay) + else: + log.trace(f"Retrieved the TextChannel for {self.__class__.__name__}") + break + else: + log.error(f"Cannot find channel with id `{self.destination}`; cannot watch users") + return False + + if self.webhook is None: + self.webhook = await self.bot.get_webhook_info(self.webhook_id) # This is `fetch_webhook` in current + if self.webhook is None: + log.error(f"Cannot find webhook with id `{self.webhook_id}`; cannot watch users") + return False + log.trace(f"Retrieved the webhook for {self.__class__.__name__}") + + log.trace(f"WatchChannel for {self.__class__.__name__} is fully initialized") + return True + + async def fetch_user_cache(self) -> bool: + """ + Fetches watched users from the API and updates the watched user cache accordingly. + + This function returns `True` if the update succeeded. + """ + + try: + data = await self.bot.api_client.get( + self.api_endpoint, + params=self.api_default_params + ) + except aiohttp.ClientResponseError as e: + log.exception( + f"Failed to fetch {self.__class__.__name__} watched users from API", + exc_info=e + ) + return False + + self.watched_users = defaultdict(dict) + + for entry in data: + user_id = entry.pop('user') + self.watched_users[user_id] = entry + + return True + + # @Cog.listener() + async def on_message(self, msg: Message): + """Queues up messages sent by watched users.""" + + if msg.author.id in self.watched_users: + if not self.consuming_messages: + self._consume_task = self.bot.loop.create_task(self.consume_messages()) + + log.trace(f"Received message: {msg.content} ({len(msg.attachments)} attachments)") + self.message_queue[msg.author.id][msg.channel.id].append(msg) + + async def consume_messages(self, delay_consumption: bool = True): + """Consumes the message queues to log watched users' messages.""" + + if delay_consumption: + log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") + await asyncio.sleep(1) + + log.trace(f"{self.__class__.__name__} started consuming the message queue") + + # Prevent losing a partly processed consumption queue after Task failure + if not self.consumption_queue: + self.consumption_queue = self.message_queue.copy() + self.message_queue.clear() + + for user_channel_queues in self.consumption_queue.values(): + for channel_queue in user_channel_queues.values(): + while channel_queue: + msg = channel_queue.popleft() + + log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") + await self.relay_message(msg) + + self.consumption_queue.clear() + + if self.message_queue: + log.trace("Channel queue not empty: Continuing consuming queues") + self._consume_task = self.bot.loop.create_task( + self.consume_messages(delay_consumption=False) + ) + else: + log.trace("Done consuming messages.") + + async def webhook_send( + self, content: Optional[str] = None, username: Optional[str] = None, + avatar_url: Optional[str] = None, embed: Optional[Embed] = None, + ): + """Sends a message to the webhook with the specified kwargs.""" + + try: + await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed) + except discord.HTTPException as exc: + log.exception( + f"Failed to send message to {self.__class__.__name__} webhook", + exc_info=exc + ) + + async def relay_message(self, msg: Message) -> None: + """Relays the message to the relevant WatchChannel""" + + last_author, last_channel, count = self.message_history + limit = BigBrotherConfig.header_message_limit + + if msg.author.id != last_author or msg.channel.id != last_channel or count >= limit: + self.message_history = [msg.author.id, msg.channel.id, 0] + + await self.send_header(msg) + + cleaned_content = msg.clean_content + + if cleaned_content: + media_urls = {embed.url for embed in msg.embeds if embed.type in ("image", "video")} + for url in URL_RE.findall(cleaned_content): + if url not in media_urls: + cleaned_content = cleaned_content.replace(url, f"`{url}`") + await self.webhook_send( + cleaned_content, + username=msg.author.display_name, + avatar_url=msg.author.avatar_url + ) + + if msg.attachments: + try: + await messages.send_attachments(msg, 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.webhook_send( + embed=e, + username=msg.author.display_name, + avatar_url=msg.author.avatar_url + ) + except discord.HTTPException as exc: + log.exception( + f"Failed to send an attachment to {self.__class__.__name__} webhook", + exc_info=exc + ) + + self.message_history[2] += 1 + + async def send_header(self, msg): + """Sends an header embed to the WatchChannel""" + + user_id = msg.author.id + + guild = self.bot.get_guild(GuildConfig.id) + actor = guild.get_member(self.watched_users[user_id]['actor']) + actor = actor.display_name if actor else self.watched_users[user_id]['actor'] + + inserted_at = self.watched_users[user_id]['inserted_at'] + date_time = datetime.datetime.strptime( + inserted_at, + "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=None) + time_delta = time_since(date_time, precision="minutes", max_units=1) + + reason = self.watched_users[user_id]['reason'] + + embed = Embed(description=( + f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})\n" + )) + embed.set_footer(text=( + f"Added {time_delta} by {actor} | " + f"Reason: {reason}" + )) + await self.webhook_send( + embed=embed, + username=msg.author.display_name, + avatar_url=msg.author.avatar_url + ) + + async def list_watched_users(self, ctx: Context, update_cache: bool = False) -> None: + """ + Gives an overview of the watched user list for this channel. + + The optional kwarg `update_cache` specifies whether the cache should + be refreshed by polling the API. + """ + + if update_cache: + if not await self.fetch_user_cache(): + e = Embed( + description=f":x: **Failed to update {self.__class__.__name__} user cache**", + color=Color.red() + ) + ctx.send(embed=e) + + lines = [] + for user_id, user_data in self.watched_users.items(): + inserted_at = user_data['inserted_at'] + date_time = datetime.datetime.strptime( + inserted_at, + "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=None) + time_delta = time_since(date_time, precision="minutes", max_units=1) + lines.append(f"• <@{user_id}> (added {time_delta})") + + await LinePaginator.paginate( + lines or ("There's nothing here yet.",), + ctx, + Embed( + title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", + color=Color.blue() + ), + empty=False + ) + + def cog_unload(self): + """Takes care of unloading the cog and cancelling the consumption task.""" + + log.trace(f"Unloading {self.__class__._name__} cog") + if not self._consume_task.done(): + self._consume_task.cancel() + try: + self._consume_task.result() + except asyncio.CancelledError as e: + log.exception( + f"The {self.__class__._name__} consume task was cancelled. Messages may be lost.", + exc_info=e + ) -- cgit v1.2.3 From 75dc0b8a53a4105caf2d362bac75a7a23c856e5c Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 23 Apr 2019 16:59:34 +0200 Subject: Changing the send_attachments utility to add WebHook support --- bot/utils/messages.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index fc38b0127..b285444c2 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -1,9 +1,9 @@ import asyncio import contextlib from io import BytesIO -from typing import Sequence +from typing import Sequence, Union -from discord import Embed, File, Message, TextChannel +from discord import Embed, File, Message, TextChannel, Webhook from discord.abc import Snowflake from discord.errors import HTTPException @@ -78,7 +78,7 @@ async def wait_for_deletion( await message.delete() -async def send_attachments(message: Message, destination: TextChannel): +async def send_attachments(message: Message, destination: Union[TextChannel, Webhook]): """ Re-uploads each attachment in a message to the given channel. @@ -97,7 +97,14 @@ async def send_attachments(message: Message, destination: TextChannel): if attachment.size <= MAX_SIZE - 512: with BytesIO() as file: await attachment.save(file) - await destination.send(file=File(file, filename=attachment.filename)) + if isinstance(destination, TextChannel): + await destination.send(file=File(file, filename=attachment.filename)) + else: + await destination.send( + file=File(file, filename=attachment.filename), + username=message.author.display_name, + avatar_url=message.author.avatar_url + ) else: large.append(attachment) except HTTPException as e: @@ -109,4 +116,11 @@ async def send_attachments(message: Message, destination: TextChannel): if large: embed = Embed(description=f"\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large)) embed.set_footer(text="Attachments exceed upload size limit.") - await destination.send(embed=embed) + if isinstance(destination, TextChannel): + await destination.send(embed=embed) + else: + await destination.send( + embed=embed, + username=message.author.display_name, + avatar_url=message.author.avatar_url + ) -- cgit v1.2.3 From 5653c52c88508f03469960cf1abab45b2a5341d5 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 23 Apr 2019 17:01:12 +0200 Subject: Adding support to the 'active' parameter when posting an infraction --- bot/utils/moderation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index fcdf3c4d5..e81186253 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -14,8 +14,8 @@ HEADERS = {"X-API-KEY": Keys.site_api} async def post_infraction( - ctx: Context, user: Union[Member, Object, User], - type: str, reason: str, expires_at: datetime = None, hidden: bool = False + ctx: Context, user: Union[Member, Object, User], type: str, reason: str, + expires_at: datetime = None, hidden: bool = False, active: bool = True ): payload = { @@ -23,7 +23,8 @@ async def post_infraction( "hidden": hidden, "reason": reason, "type": type, - "user": user.id + "user": user.id, + "active": active } if expires_at: payload['expires_at'] = expires_at.isoformat() -- cgit v1.2.3 From cc5e439f1f9a313c857be33d7d8ab1dddd6a1607 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 23 Apr 2019 17:01:53 +0200 Subject: Adding the BigBrother watchchannel class --- bot/cogs/watchchannels/__init__.py | 12 ++++ bot/cogs/watchchannels/bigbrother.py | 119 +++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 bot/cogs/watchchannels/__init__.py create mode 100644 bot/cogs/watchchannels/bigbrother.py diff --git a/bot/cogs/watchchannels/__init__.py b/bot/cogs/watchchannels/__init__.py new file mode 100644 index 000000000..e390bf062 --- /dev/null +++ b/bot/cogs/watchchannels/__init__.py @@ -0,0 +1,12 @@ +import logging + +from .bigbrother import BigBrother + + +log = logging.getLogger(__name__) + + +def setup(bot): + log.trace("Started adding BigBrother cog") + bot.add_cog(BigBrother(bot)) + log.trace("Finished adding BigBrother cog") diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py new file mode 100644 index 000000000..a7a66e6dc --- /dev/null +++ b/bot/cogs/watchchannels/bigbrother.py @@ -0,0 +1,119 @@ +import logging +from collections import ChainMap + +from discord import Color, Embed, User +from discord.ext.commands import Bot, Context, group + +from bot.constants import ( + Channels, Roles +) +from bot.decorators import with_role +from bot.utils.moderation import post_infraction +from .watchchannel import WatchChannel + +log = logging.getLogger(__name__) + + +class BigBrother(WatchChannel): + """User monitoring to assist with moderation""" + + def __init__(self, bot): + super().__init__(bot) + self.log = log + + self.destination = Channels.big_brother_logs + self.webhook_id = 569096053333164052 + self.api_endpoint = 'bot/infractions' + self.api_default_params = {'active': 'true', 'type': 'watch'} + + @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def bigbrother_group(self, ctx: Context) -> None: + """Monitors users by relaying their messages to the BigBrother watch channel""" + + await ctx.invoke(self.bot.get_command("help"), "bigbrother") + + @bigbrother_group.command(name='watched', aliases=('all',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watched_command(self, ctx: Context, update_cache: bool = False) -> None: + """ + Shows the users that are currently being monitored in BigBrother. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + + await self.list_watched_users(ctx, update_cache) + + @bigbrother_group.command(name='watch', aliases=('w',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watch_command(self, ctx: Context, user: User, *, reason: str) -> None: + """ + Relay messages sent by the given `user` to the `#big-brother-logs` channel. + + A `reason` for adding the user to BigBrother is required and will displayed + in the header when relaying messages of this user to the watchchannel. + """ + + await self.fetch_user_cache() + + if user.id in self.watched_users: + e = Embed( + description=":x: **The specified user is already being watched**", + color=Color.red() + ) + return await ctx.send(embed=e) + + response = await post_infraction( + ctx, user, type='watch', reason=reason, hidden=True + ) + if response is not None: + self.watched_users[user.id] = response + e = Embed( + description=f":white_check_mark: **Messages sent by {user} will now be relayed**", + color=Color.green() + ) + return await ctx.send(embed=e) + + @bigbrother_group.command(name='unwatch', aliases=('uw',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def unwatch_command(self, ctx: Context, user: User, *, reason: str) -> None: + """Stop relaying messages by the given `user`.""" + + active_watches = await self.bot.api_client.get( + self.api_endpoint, + params=ChainMap( + self.api_default_params, + {"user__id": str(user.id)} + ) + ) + if active_watches: + [infraction] = active_watches + log.trace(infraction) + await self.bot.api_client.patch( + f"{self.api_endpoint}/{infraction['id']}", + json={'active': False} + ) + await post_infraction( + ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False + ) + e = Embed( + description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", + color=Color.green() + ) + return await ctx.send(embed=e) + self.watched_users.pop(str(user.id), None) + self.message_queue.pop(str(user.id), None) + self.consumption_queue.pop(str(user.id), None) + else: + e = Embed( + description=":x: **The specified user is currently not being watched**", + color=Color.red() + ) + return await ctx.send(embed=e) + + @bigbrother_group.command(name='debug') + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def debug(self, ctx): + for data in self.watched_users.values(): + await ctx.send(data) -- cgit v1.2.3 From 9e2e8580701f6ce74a75ee96436cf96fcdc24f07 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 23 Apr 2019 17:09:36 +0200 Subject: Cleaning up debug functions and unnecessary imports --- bot/cogs/watchchannels/bigbrother.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index a7a66e6dc..5e1f2c30b 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -2,7 +2,7 @@ import logging from collections import ChainMap from discord import Color, Embed, User -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Context, group from bot.constants import ( Channels, Roles @@ -111,9 +111,3 @@ class BigBrother(WatchChannel): color=Color.red() ) return await ctx.send(embed=e) - - @bigbrother_group.command(name='debug') - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def debug(self, ctx): - for data in self.watched_users.values(): - await ctx.send(data) -- cgit v1.2.3 From 59a75181e4f6c3a0994b0cc0603441ad0b1225e6 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:40:47 +0200 Subject: Adding proxy channel + improved methods to watchchannel --- bot/cogs/watchchannels/watchchannel.py | 117 +++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 856211139..5f9d8d1dd 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -8,12 +8,10 @@ from typing import Optional import aiohttp import discord -from discord import Color, Embed, Message, errors -from discord.ext.commands import Bot, Context +from discord import Color, Embed, Message, Object, errors +from discord.ext.commands import BadArgument, Bot, Context -from bot.constants import ( - BigBrother as BigBrotherConfig, Guild as GuildConfig -) +from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig from bot.pagination import LinePaginator from bot.utils import messages from bot.utils.time import time_since @@ -23,6 +21,19 @@ log = logging.getLogger(__name__) URL_RE = re.compile(r"(https?://[^\s]+)") +def proxy_user(user_id: str) -> Object: + try: + user_id = int(user_id) + except ValueError: + raise BadArgument + user = Object(user_id) + user.mention = user.id + user.display_name = f"<@{user.id}>" + user.avatar_url_as = lambda static_format: None + user.bot = False + return user + + class WatchChannel(ABC): """ Base class for WatchChannels @@ -41,12 +52,11 @@ class WatchChannel(ABC): __init__ of the child after the super().__init__(*args, **kwargs) call. """ - self.bot = bot # These attributes need to be overwritten in the child class self.destination = None # Channels.big_brother_logs - self.webhook_id = None # Some t.b.d. constant + self.webhook_id = None # Webhooks.big_brother self.api_endpoint = None # 'bot/infractions' self.api_default_params = None # {'active': 'true', 'type': 'watch'} @@ -54,7 +64,7 @@ class WatchChannel(ABC): self._consume_task = None self.watched_users = defaultdict(dict) self.message_queue = defaultdict(lambda: defaultdict(deque)) - self.consumption_queue = None + self.consumption_queue = {} self.retries = 5 self.retry_delay = 10 self.channel = None @@ -66,14 +76,13 @@ class WatchChannel(ABC): @property def consuming_messages(self) -> bool: """Checks if a consumption task is currently running.""" - if self._consume_task is None: return False if self._consume_task.done(): exc = self._consume_task.exception() if exc: - log.exception( + self.log.exception( f"{self.__class__.__name__} consume task has failed with:", exc_info=exc ) @@ -81,16 +90,14 @@ class WatchChannel(ABC): return True - # @Cog.listener() async def start_watchchannel(self) -> None: """Retrieves watched users from the API.""" - await self.bot.wait_until_ready() if await self.initialize_channel() and await self.fetch_user_cache(): - log.trace(f"Started the {self.__class__.__name__} WatchChannel") + self.log.trace(f"Started the {self.__class__.__name__} WatchChannel") else: - log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") + self.log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") async def initialize_channel(self) -> bool: """ @@ -101,33 +108,30 @@ class WatchChannel(ABC): channel and webhook were initialized successfully. this function will return `True`. """ - if self.channel is None: for attempt in range(1, self.retries + 1): self.channel = self.bot.get_channel(self.destination) if self.channel is None: - log.error(f"Failed to get the {self.__class__.__name__} channel; cannot watch users") + self.log.error(f"Failed to get the {self.__class__.__name__} channel; cannot watch users") if attempt < self.initialization_retries: - log.error( - f"Attempt {attempt}/{self.retries}; " - f"Retrying in {self.retry_delay} seconds...") + self.log.error(f"Attempt {attempt}/{self.retries}; Retrying in {self.retry_delay} seconds...") await asyncio.sleep(self.retry_delay) else: - log.trace(f"Retrieved the TextChannel for {self.__class__.__name__}") + self.log.trace(f"Retrieved the TextChannel for {self.__class__.__name__}") break else: - log.error(f"Cannot find channel with id `{self.destination}`; cannot watch users") + self.log.error(f"Cannot get channel with id `{self.destination}`; cannot watch users") return False if self.webhook is None: self.webhook = await self.bot.get_webhook_info(self.webhook_id) # This is `fetch_webhook` in current if self.webhook is None: - log.error(f"Cannot find webhook with id `{self.webhook_id}`; cannot watch users") + self.log.error(f"Cannot get webhook with id `{self.webhook_id}`; cannot watch users") return False - log.trace(f"Retrieved the webhook for {self.__class__.__name__}") + self.log.trace(f"Retrieved the webhook for {self.__class__.__name__}") - log.trace(f"WatchChannel for {self.__class__.__name__} is fully initialized") + self.log.trace(f"WatchChannel for {self.__class__.__name__} is fully initialized") return True async def fetch_user_cache(self) -> bool: @@ -136,14 +140,13 @@ class WatchChannel(ABC): This function returns `True` if the update succeeded. """ - try: data = await self.bot.api_client.get( self.api_endpoint, params=self.api_default_params ) except aiohttp.ClientResponseError as e: - log.exception( + self.log.exception( f"Failed to fetch {self.__class__.__name__} watched users from API", exc_info=e ) @@ -157,25 +160,22 @@ class WatchChannel(ABC): return True - # @Cog.listener() async def on_message(self, msg: Message): """Queues up messages sent by watched users.""" - if msg.author.id in self.watched_users: if not self.consuming_messages: self._consume_task = self.bot.loop.create_task(self.consume_messages()) - log.trace(f"Received message: {msg.content} ({len(msg.attachments)} attachments)") + self.log.trace(f"Received message: {msg.content} ({len(msg.attachments)} attachments)") self.message_queue[msg.author.id][msg.channel.id].append(msg) async def consume_messages(self, delay_consumption: bool = True): """Consumes the message queues to log watched users' messages.""" - if delay_consumption: - log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") + self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") await asyncio.sleep(1) - log.trace(f"{self.__class__.__name__} started consuming the message queue") + self.log.trace(f"{self.__class__.__name__} started consuming the message queue") # Prevent losing a partly processed consumption queue after Task failure if not self.consumption_queue: @@ -187,36 +187,34 @@ class WatchChannel(ABC): while channel_queue: msg = channel_queue.popleft() - log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") + self.log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") await self.relay_message(msg) self.consumption_queue.clear() if self.message_queue: - log.trace("Channel queue not empty: Continuing consuming queues") + self.log.trace("Channel queue not empty: Continuing consuming queues") self._consume_task = self.bot.loop.create_task( self.consume_messages(delay_consumption=False) ) else: - log.trace("Done consuming messages.") + self.log.trace("Done consuming messages.") async def webhook_send( self, content: Optional[str] = None, username: Optional[str] = None, avatar_url: Optional[str] = None, embed: Optional[Embed] = None, ): """Sends a message to the webhook with the specified kwargs.""" - try: await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed) except discord.HTTPException as exc: - log.exception( + self.log.exception( f"Failed to send message to {self.__class__.__name__} webhook", exc_info=exc ) async def relay_message(self, msg: Message) -> None: """Relays the message to the relevant WatchChannel""" - last_author, last_channel, count = self.message_history limit = BigBrotherConfig.header_message_limit @@ -252,7 +250,7 @@ class WatchChannel(ABC): avatar_url=msg.author.avatar_url ) except discord.HTTPException as exc: - log.exception( + self.log.exception( f"Failed to send an attachment to {self.__class__.__name__} webhook", exc_info=exc ) @@ -261,7 +259,6 @@ class WatchChannel(ABC): async def send_header(self, msg): """Sends an header embed to the WatchChannel""" - user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) @@ -304,16 +301,13 @@ class WatchChannel(ABC): description=f":x: **Failed to update {self.__class__.__name__} user cache**", color=Color.red() ) - ctx.send(embed=e) + await ctx.send(embed=e) + return lines = [] for user_id, user_data in self.watched_users.items(): inserted_at = user_data['inserted_at'] - date_time = datetime.datetime.strptime( - inserted_at, - "%Y-%m-%dT%H:%M:%S.%fZ" - ).replace(tzinfo=None) - time_delta = time_since(date_time, precision="minutes", max_units=1) + time_delta = self._get_time_delta(inserted_at) lines.append(f"• <@{user_id}> (added {time_delta})") await LinePaginator.paginate( @@ -326,16 +320,43 @@ class WatchChannel(ABC): empty=False ) + @staticmethod + def _get_time_delta(time_string: str) -> str: + """Returns the time in human-readable time delta format""" + + date_time = datetime.datetime.strptime( + time_string, + "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=None) + time_delta = time_since(date_time, precision="minutes", max_units=1) + + return time_delta + + @staticmethod + def _get_human_readable(time_string: str, output_format: str = "%Y-%m-%d %H:%M:%S") -> str: + date_time = datetime.datetime.strptime( + time_string, + "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=None) + return date_time.strftime(output_format) + + def _remove_user(self, user_id: int) -> None: + """Removes user from the WatchChannel""" + + self.watched_users.pop(user_id, None) + self.message_queue.pop(user_id, None) + self.consumption_queue.pop(user_id, None) + def cog_unload(self): """Takes care of unloading the cog and cancelling the consumption task.""" - log.trace(f"Unloading {self.__class__._name__} cog") + self.log.trace(f"Unloading {self.__class__._name__} cog") if not self._consume_task.done(): self._consume_task.cancel() try: self._consume_task.result() except asyncio.CancelledError as e: - log.exception( + self.log.exception( f"The {self.__class__._name__} consume task was cancelled. Messages may be lost.", exc_info=e ) -- cgit v1.2.3 From e84b04be18d0a736963af43a930899f47282b577 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:42:43 +0200 Subject: Adding proxy_user to type hints, adding anti-bot watch, and tweaking user feedback --- bot/cogs/watchchannels/bigbrother.py | 43 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 5e1f2c30b..55805cf87 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -1,15 +1,14 @@ import logging from collections import ChainMap +from typing import Union from discord import Color, Embed, User from discord.ext.commands import Context, group -from bot.constants import ( - Channels, Roles -) +from bot.constants import Channels, Roles, Webhooks from bot.decorators import with_role from bot.utils.moderation import post_infraction -from .watchchannel import WatchChannel +from .watchchannel import WatchChannel, proxy_user log = logging.getLogger(__name__) @@ -19,43 +18,51 @@ class BigBrother(WatchChannel): def __init__(self, bot): super().__init__(bot) - self.log = log + self.log = log # to ensure logs created in the super() get the name of this file self.destination = Channels.big_brother_logs - self.webhook_id = 569096053333164052 + self.webhook_id = Webhooks.big_brother self.api_endpoint = 'bot/infractions' - self.api_default_params = {'active': 'true', 'type': 'watch'} + self.api_default_params = { + 'active': 'true', 'type': 'watch', 'ordering': '-inserted_at' + } @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def bigbrother_group(self, ctx: Context) -> None: """Monitors users by relaying their messages to the BigBrother watch channel""" - await ctx.invoke(self.bot.get_command("help"), "bigbrother") - @bigbrother_group.command(name='watched', aliases=('all',)) + @bigbrother_group.command(name='watched', aliases=('all', 'list')) @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watched_command(self, ctx: Context, update_cache: bool = False) -> None: + async def watched_command(self, ctx: Context, update_cache: bool = True) -> None: """ Shows the users that are currently being monitored in BigBrother. The optional kwarg `update_cache` can be used to update the user cache using the API before listing the users. """ - await self.list_watched_users(ctx, update_cache) @bigbrother_group.command(name='watch', aliases=('w',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watch_command(self, ctx: Context, user: User, *, reason: str) -> None: + async def watch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: """ Relay messages sent by the given `user` to the `#big-brother-logs` channel. A `reason` for adding the user to BigBrother is required and will displayed in the header when relaying messages of this user to the watchchannel. """ + if user.bot: + e = Embed( + description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", + color=Color.red() + ) + return await ctx.send(embed=e) - await self.fetch_user_cache() + if not await self.fetch_user_cache(): + log.error("Failed to update user cache; can't watch user {user}") + return if user.id in self.watched_users: e = Embed( @@ -77,9 +84,8 @@ class BigBrother(WatchChannel): @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def unwatch_command(self, ctx: Context, user: User, *, reason: str) -> None: + async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: """Stop relaying messages by the given `user`.""" - active_watches = await self.bot.api_client.get( self.api_endpoint, params=ChainMap( @@ -89,7 +95,6 @@ class BigBrother(WatchChannel): ) if active_watches: [infraction] = active_watches - log.trace(infraction) await self.bot.api_client.patch( f"{self.api_endpoint}/{infraction['id']}", json={'active': False} @@ -101,10 +106,8 @@ class BigBrother(WatchChannel): description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", color=Color.green() ) - return await ctx.send(embed=e) - self.watched_users.pop(str(user.id), None) - self.message_queue.pop(str(user.id), None) - self.consumption_queue.pop(str(user.id), None) + await ctx.send(embed=e) + self._remove_user(user.id) else: e = Embed( description=":x: **The specified user is currently not being watched**", -- cgit v1.2.3 From feaf3ff8c3d6970dd3156328d2eacc382f0ebe6b Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:43:17 +0200 Subject: Adding TalentPool to the watchchannels --- bot/cogs/watchchannels/__init__.py | 7 +- bot/cogs/watchchannels/talentpool.py | 267 +++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 bot/cogs/watchchannels/talentpool.py diff --git a/bot/cogs/watchchannels/__init__.py b/bot/cogs/watchchannels/__init__.py index e390bf062..ac7713803 100644 --- a/bot/cogs/watchchannels/__init__.py +++ b/bot/cogs/watchchannels/__init__.py @@ -1,12 +1,15 @@ import logging from .bigbrother import BigBrother +from .talentpool import TalentPool log = logging.getLogger(__name__) def setup(bot): - log.trace("Started adding BigBrother cog") bot.add_cog(BigBrother(bot)) - log.trace("Finished adding BigBrother cog") + log.info("Cog loaded: BigBrother") + + bot.add_cog(TalentPool(bot)) + log.info("Cog loaded: TalentPool") diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py new file mode 100644 index 000000000..773547bab --- /dev/null +++ b/bot/cogs/watchchannels/talentpool.py @@ -0,0 +1,267 @@ +import logging +import textwrap +from collections import ChainMap +from typing import Union + +from aiohttp.client_exceptions import ClientResponseError +from discord import Color, Embed, Member, User +from discord.ext.commands import Context, group + +from bot.constants import Channels, Guild, Roles, Webhooks +from bot.decorators import with_role +from bot.pagination import LinePaginator +from .watchchannel import WatchChannel, proxy_user + +log = logging.getLogger(__name__) +STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- In constants after the merge? + + +class TalentPool(WatchChannel): + """A TalentPool for helper nominees""" + def __init__(self, bot): + super().__init__(bot) + self.log = log # to ensure logs created in the super() get the name of this file + + self.destination = Channels.big_brother_logs + self.webhook_id = Webhooks.talent_pool + self.api_endpoint = 'bot/nominations' + self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'} + + @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def nomination_group(self, ctx: Context) -> None: + """Highlights the activity of helper nominees by relaying their messages to TalentPool.""" + + await ctx.invoke(self.bot.get_command("help"), "talentpool") + + @nomination_group.command(name='watched', aliases=('all', 'list')) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watched_command(self, ctx: Context, update_cache: bool = True) -> None: + """ + Shows the users that are currently being monitored in BigBrother. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + await self.list_watched_users(ctx, update_cache) + + @nomination_group.command(name='watch', aliases=('w', 'add', 'a')) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def watch_command(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: + """ + Relay messages sent by the given `user` to the `#big-brother-logs` channel. + + A `reason` for adding the user to BigBrother is required and will displayed + in the header when relaying messages of this user to the watchchannel. + """ + if user.bot: + e = Embed( + description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", + color=Color.red() + ) + return await ctx.send(embed=e) + + if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): + e = Embed( + description=f":x: **Nominating staff members, eh? You cheeky bastard.**", + color=Color.red() + ) + return await ctx.send(embed=e) + + if not await self.fetch_user_cache(): + log.error(f"Failed to update user cache; can't watch user {user}") + e = Embed( + description=f":x: **Failed to update the user cache; can't add {user}**", + color=Color.red() + ) + return await ctx.send(embed=e) + + if user.id in self.watched_users: + e = Embed( + description=":x: **The specified user is already in the TalentPool**", + color=Color.red() + ) + return await ctx.send(embed=e) + + # Manual request with `raise_for_status` as False becausse we want the actual response + session = self.bot.api_client.session + url = self.bot.api_client._url_for(self.api_endpoint) + kwargs = { + 'json': { + 'actor': ctx.author.id, + 'reason': reason, + 'user': user.id + }, + 'raise_for_status': False, + } + async with session.post(url, **kwargs) as resp: + response_data = await resp.json() + + if resp.status == 400 and response_data.get('user', False): + e = Embed( + description=":x: **The specified user can't be found in the database tables**", + color=Color.red() + ) + return await ctx.send(embed=e) + elif resp.status >= 400: + resp.raise_for_status() + + self.watched_users[user.id] = response_data + e = Embed( + description=f":white_check_mark: **Messages sent by {user} will now be relayed**", + color=Color.green() + ) + return await ctx.send(embed=e) + + @nomination_group.command(name='history', aliases=('info', 'search')) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def history_command(self, ctx: Context, user: Union[User, proxy_user]) -> None: + """Shows the specified user's nomination history""" + result = await self.bot.api_client.get( + self.api_endpoint, + params={ + 'user__id': str(user.id), + 'ordering': "-active,-inserted_at" + } + ) + if not result: + e = Embed( + description=":warning: **This user has never been nominated**", + color=Color.blue() + ) + return await ctx.send(embed=e) + + embed = Embed( + title=f"Nominations for {user.display_name} `({user.id})`", + color=Color.blue() + ) + lines = [self._nomination_to_string(nomination) for nomination in result] + await LinePaginator.paginate( + lines, + ctx=ctx, + embed=embed, + empty=True, + max_lines=3, + max_size=1000 + ) + + @nomination_group.command(name='unwatch', aliases=('end', )) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: + """ + Ends the active nomination of the specified user with the given reason. + + Providing a `reason` is required. + """ + active_nomination = await self.bot.api_client.get( + self.api_endpoint, + params=ChainMap( + self.api_default_params, + {"user__id": str(user.id)} + ) + ) + + if not active_nomination: + e = Embed( + description=":x: **The specified user does not have an active Nomination**", + color=Color.red() + ) + return await ctx.send(embed=e) + + [nomination] = active_nomination + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination['id']}/end", + json={'unnominate_reason': reason} + ) + e = Embed( + description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", + color=Color.green() + ) + await ctx.send(embed=e) + self._remove_user(user.id) + + @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def nomination_edit_group(self, ctx: Context) -> None: + """Highlights the activity of helper nominees by relaying their messages to TalentPool.""" + + await ctx.invoke(self.bot.get_command("help"), "talentpool", "edit") + + @nomination_edit_group.command(name='reason') + @with_role(Roles.owner, Roles.admin, Roles.moderator) + async def edit_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: + """ + Edits the reason/unnominate reason for the nomination with the given `id` depending on the status. + + If the nomination is active, the reason for nominating the user will be edited; + If the nomination is no longer active, the reason for ending the nomination will be edited instead. + """ + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ClientResponseError as e: + if e.status == 404: + self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") + e = Embed( + description=f":x: **Can't find a nomination with id `{nomination_id}`**", + color=Color.red() + ) + return await ctx.send(embed=e) + else: + raise + + field = "reason" if nomination["active"] else "unnominate_reason" + + self.log.trace(f"Changing {field} for nomination with id {nomination_id} to {reason}") + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={field: reason} + ) + + e = Embed( + description=f":white_check_mark: **Updated the {field} of the nomination!**", + color=Color.green() + ) + await ctx.send(embed=e) + + def _nomination_to_string(self, nomination_object): + """Creates a string representation of a nomination""" + guild = self.bot.get_guild(Guild.id) + + actor_id = nomination_object["actor"] + actor = guild.get_member(actor_id) + + active = nomination_object["active"] + log.debug(active) + log.debug(type(nomination_object["inserted_at"])) + + start_date = self._get_human_readable(nomination_object["inserted_at"]) + if active: + lines = textwrap.dedent( + f""" + =============== + Status: **Active** + Date: {start_date} + Actor: {actor.mention if actor else actor_id} + Reason: {nomination_object["reason"]} + Nomination ID: `{nomination_object["id"]}` + =============== + """ + ) + else: + end_date = self._get_human_readable(nomination_object["unwatched_at"]) + lines = textwrap.dedent( + f""" + =============== + Status: Inactive + Date: {start_date} + Actor: {actor.mention if actor else actor_id} + Reason: {nomination_object["reason"]} + + End date: {end_date} + Unwatch reason: {nomination_object["unnominate_reason"]} + Nomination ID: `{nomination_object["id"]}` + =============== + """ + ) + + return lines.strip() -- cgit v1.2.3 From 2d296ca5482751edabe7a8dc485c3e626c3a1546 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:43:43 +0200 Subject: Tweaking aliases to use the correct converters --- bot/cogs/alias.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 2ce4a51e3..d892c7b87 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -1,11 +1,13 @@ import inspect import logging +from typing import Union -from discord import Colour, Embed, User +from discord import Colour, Embed, Member, User from discord.ext.commands import ( Command, Context, clean_content, command, group ) +from bot.cogs.watchchannels.watchchannel import proxy_user from bot.converters import TagNameConverter from bot.pagination import LinePaginator @@ -71,23 +73,23 @@ class Alias: @command(name="watch", hidden=True) async def bigbrother_watch_alias( - self, ctx, user: User, *, reason: str = None + self, ctx, user: Union[Member, User, proxy_user], *, reason: str = None ): """ - Alias for invoking bigbrother watch user [text_channel]. + Alias for invoking bigbrother watch [user] [reason]. """ await self.invoke(ctx, "bigbrother watch", user, reason=reason) @command(name="unwatch", hidden=True) - async def bigbrother_unwatch_alias(self, ctx, user: User): + async def bigbrother_unwatch_alias( + self, ctx, user: Union[User, proxy_user], *, reason: str = None + ): """ - Alias for invoking bigbrother unwatch user. - - user: discord.User - A user instance to unwatch + Alias for invoking bigbrother unwatch [user] [reason]. """ - await self.invoke(ctx, "bigbrother unwatch", user) + await self.invoke(ctx, "bigbrother unwatch", user, reason=reason) @command(name="home", hidden=True) async def site_home_alias(self, ctx): @@ -175,6 +177,26 @@ class Alias: await self.invoke(ctx, "docs get", symbol) + @command(name="nominate", hidden=True) + async def nomination_add_alias( + self, ctx, user: Union[Member, User, proxy_user], *, reason: str = None + ): + """ + Alias for invoking talentpool add [user] [reason]. + """ + + await self.invoke(ctx, "talentpool add", user, reason=reason) + + @command(name="unnominate", hidden=True) + async def nomination_end_alias( + self, ctx, user: Union[User, proxy_user], *, reason: str = None + ): + """ + Alias for invoking nomination end [user] [reason]. + """ + + await self.invoke(ctx, "nomination end", user, reason=reason) + def setup(bot): bot.add_cog(Alias(bot)) -- cgit v1.2.3 From 63aa34d474dad4e9062e5e690d5004829599442b Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:44:45 +0200 Subject: Adding Webhooks to constants --- bot/constants.py | 8 ++++++++ config-default.yml | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 0bd950a7d..3c129afd2 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -339,6 +339,14 @@ class Channels(metaclass=YAMLGetter): verification: int +class Webhooks(metaclass=YAMLGetter): + section = "guild" + subsection = "webhooks" + + talent_pool: int + big_brother: int + + class Roles(metaclass=YAMLGetter): section = "guild" subsection = "roles" diff --git a/config-default.yml b/config-default.yml index 7854b5db9..8bfc84cb4 100644 --- a/config-default.yml +++ b/config-default.yml @@ -128,6 +128,10 @@ guild: helpers: 267630620367257601 rockstars: &ROCKSTARS_ROLE 458226413825294336 + webhooks: + talent_pool: 569145364800602132 + big_brother: 569133704568373283 + filter: -- cgit v1.2.3 From 38f03f01ae00f5fcb04dfd1c64e3bda1a49ef4d8 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 21:53:38 +0200 Subject: Truncating long names that misalign logs + adding 3 chars to account for watchchannels --- bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/__init__.py b/bot/__init__.py index 54550842e..fecd7ceb3 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -55,7 +55,7 @@ else: logging.basicConfig( - format="%(asctime)s pd.beardfist.com Bot: | %(name)30s | %(levelname)8s | %(message)s", + format="%(asctime)s pd.beardfist.com Bot: | %(name)33.33s | %(levelname)8s | %(message)s", datefmt="%b %d %H:%M:%S", level=logging.TRACE if DEBUG_MODE else logging.INFO, handlers=logging_handlers -- cgit v1.2.3 From 725656f471715fe96c92fac3b0155213f17029c1 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 27 Jun 2019 22:09:34 +0200 Subject: Removing the old talentpool/bigbrother files + changing the extension loader --- bot/__main__.py | 2 +- bot/cogs/bigbrother.py | 258 ------------------------------------------------ bot/cogs/nominations.py | 120 ---------------------- 3 files changed, 1 insertion(+), 379 deletions(-) delete mode 100644 bot/cogs/bigbrother.py delete mode 100644 bot/cogs/nominations.py diff --git a/bot/__main__.py b/bot/__main__.py index 8afec2718..44d4d9c02 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -38,7 +38,6 @@ bot.load_extension("bot.cogs.modlog") # Commands, etc bot.load_extension("bot.cogs.antispam") -bot.load_extension("bot.cogs.bigbrother") bot.load_extension("bot.cogs.bot") bot.load_extension("bot.cogs.clean") bot.load_extension("bot.cogs.cogs") @@ -69,6 +68,7 @@ bot.load_extension("bot.cogs.sync") bot.load_extension("bot.cogs.tags") bot.load_extension("bot.cogs.token_remover") bot.load_extension("bot.cogs.utils") +bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.wolfram") bot.run(BotConfig.token) diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py deleted file mode 100644 index df7a0b576..000000000 --- a/bot/cogs/bigbrother.py +++ /dev/null @@ -1,258 +0,0 @@ -import asyncio -import logging -import re -from collections import defaultdict, deque -from typing import List, Union - -from discord import Color, Embed, Guild, Member, Message, User -from discord.ext.commands import Bot, Context, group - -from bot.constants import ( - BigBrother as BigBrotherConfig, Channels, Emojis, Guild as GuildConfig, Roles -) -from bot.decorators import with_role -from bot.pagination import LinePaginator -from bot.utils import messages -from bot.utils.moderation import post_infraction - -log = logging.getLogger(__name__) - -URL_RE = re.compile(r"(https?://[^\s]+)") - - -class BigBrother: - """User monitoring to assist with moderation.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.watched_users = set() # { user_id } - self.channel_queues = defaultdict(lambda: defaultdict(deque)) # { user_id: { channel_id: queue(messages) } - self.last_log = [None, None, 0] # [user_id, channel_id, message_count] - self.consuming = False - - def update_cache(self, api_response: List[dict]): - """ - Updates the internal cache of watched users from the given `api_response`. - This function will only add (or update) existing keys, it will not delete - keys that were not present in the API response. - A user is only added if the bot can find a channel - with the given `channel_id` in its channel cache. - """ - - for entry in api_response: - user_id = entry['user'] - self.watched_users.add(user_id) - - async def on_ready(self): - """Retrieves watched users from the API.""" - - self.channel = self.bot.get_channel(Channels.big_brother_logs) - if self.channel is None: - log.error("Cannot find Big Brother channel. Cannot watch users.") - else: - data = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'watch' - } - ) - self.update_cache(data) - - async def on_member_ban(self, guild: Guild, user: Union[User, Member]): - if guild.id == GuildConfig.id and user.id in self.watched_users: - [active_watch] = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'watch', - 'user__id': str(user.id) - } - ) - await self.bot.api_client.put( - 'bot/infractions/' + str(active_watch['id']), - json={'active': False} - ) - self.watched_users.remove(user.id) - del self.channel_queues[user.id] - await self.channel.send( - f"{Emojis.bb_message}:hammer: {user} got banned, so " - f"`BigBrother` will no longer relay their messages." - ) - - async def on_message(self, msg: Message): - """Queues up messages sent by watched users.""" - - if msg.author.id in self.watched_users: - if not self.consuming: - self.bot.loop.create_task(self.consume_messages()) - - log.trace(f"Received message: {msg.content} ({len(msg.attachments)} attachments)") - self.channel_queues[msg.author.id][msg.channel.id].append(msg) - - async def consume_messages(self): - """Consumes the message queues to log watched users' messages.""" - - if not self.consuming: - self.consuming = True - log.trace("Sleeping before consuming...") - await asyncio.sleep(BigBrotherConfig.log_delay) - - log.trace("Begin consuming messages.") - channel_queues = self.channel_queues.copy() - self.channel_queues.clear() - for _, queues in channel_queues.items(): - for queue in queues.values(): - while queue: - msg = queue.popleft() - log.trace(f"Consuming message: {msg.clean_content} ({len(msg.attachments)} attachments)") - - self.last_log[2] += 1 # Increment message count. - await self.send_header(msg) - await self.log_message(msg) - - if self.channel_queues: - log.trace("Queue not empty; continue consumption.") - self.bot.loop.create_task(self.consume_messages()) - else: - log.trace("Done consuming messages.") - self.consuming = False - - async def send_header(self, message: Message): - """ - Sends a log message header to the given channel. - - A header is only sent if the user or channel are different than the previous, or if the configured message - limit for a single header has been exceeded. - - :param message: the first message in the queue - """ - - last_user, last_channel, msg_count = self.last_log - limit = BigBrotherConfig.header_message_limit - - # Send header if user/channel are different or if message limit exceeded. - if message.author.id != last_user or message.channel.id != last_channel or msg_count > limit: - self.last_log = [message.author.id, message.channel.id, 0] - - embed = Embed(description=f"{message.author.mention} in [#{message.channel.name}]({message.jump_url})") - embed.set_author(name=message.author.nick or message.author.name, icon_url=message.author.avatar_url) - await self.channel.send(embed=embed) - - async def log_message(self, message: Message): - """ - Logs a watched user's message in the given channel. - - Attachments are also sent. All non-image or non-video URLs are put in inline code blocks to prevent preview - embeds from being automatically generated. - - :param message: the message to log - """ - - content = message.clean_content - if content: - # Put all non-media URLs in inline code blocks. - media_urls = {embed.url for embed in message.embeds if embed.type in ("image", "video")} - for url in URL_RE.findall(content): - if url not in media_urls: - content = content.replace(url, f"`{url}`") - - await self.channel.send(content) - - await messages.send_attachments(message, self.channel) - - @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def bigbrother_group(self, ctx: Context): - """Monitor users, NSA-style.""" - - await ctx.invoke(self.bot.get_command("help"), "bigbrother") - - @bigbrother_group.command(name='watched', aliases=('all',)) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watched_command(self, ctx: Context, from_cache: bool = True): - """ - Shows all users that are currently monitored and in which channel. - By default, the users are returned from the cache. - If this is not desired, `from_cache` can be given as a falsy value, e.g. e.g. 'no'. - """ - - if from_cache: - lines = tuple(f"• <@{user_id}>" for user_id in self.watched_users) - await LinePaginator.paginate( - lines or ("There's nothing here yet.",), - ctx, - Embed(title="Watched users (cached)", color=Color.blue()), - empty=False - ) - - else: - active_watches = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'watch' - } - ) - self.update_cache(active_watches) - lines = tuple( - f"• <@{entry['user']}>: {entry['reason'] or '*no reason provided*'}" - for entry in active_watches - ) - - await LinePaginator.paginate( - lines or ("There's nothing here yet.",), - ctx, - Embed(title="Watched users", color=Color.blue()), - empty=False - ) - - @bigbrother_group.command(name='watch', aliases=('w',)) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watch_command(self, ctx: Context, user: User, *, reason: str): - """ - Relay messages sent by the given `user` to the `#big-brother-logs` channel - - A `reason` for watching is required, which is added for the user to be watched as a - note (aka: shadow warning) - """ - - if user.id in self.watched_users: - return await ctx.send(":x: That user is already watched.") - - await post_infraction( - ctx, user, type='watch', reason=reason, hidden=True - ) - self.watched_users.add(user.id) - await ctx.send(f":ok_hand: will now relay messages sent by {user}") - - @bigbrother_group.command(name='unwatch', aliases=('uw',)) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def unwatch_command(self, ctx: Context, user: User): - """Stop relaying messages by the given `user`.""" - - active_watches = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'watch', - 'user__id': str(user.id) - } - ) - if active_watches: - [infraction] = active_watches - await self.bot.api_client.patch( - 'bot/infractions/' + str(infraction['id']), - json={'active': False} - ) - await ctx.send(f":ok_hand: will no longer relay messages sent by {user}") - self.watched_users.remove(user.id) - if user.id in self.channel_queues: - del self.channel_queues[user.id] - else: - await ctx.send(":x: that user is currently not being watched") - - -def setup(bot: Bot): - bot.add_cog(BigBrother(bot)) - log.info("Cog loaded: BigBrother") diff --git a/bot/cogs/nominations.py b/bot/cogs/nominations.py deleted file mode 100644 index 93ee0d885..000000000 --- a/bot/cogs/nominations.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging - -from discord import Color, Embed, User -from discord.ext.commands import Context, group - -from bot.cogs.bigbrother import BigBrother, Roles -from bot.constants import Channels -from bot.decorators import with_role -from bot.pagination import LinePaginator - - -log = logging.getLogger(__name__) - - -class Nominations(BigBrother): - """Monitor potential helpers, NSA-style.""" - - async def on_ready(self): - """Retrieve nominees from the API.""" - - self.channel = self.bot.get_channel(Channels.talent_pool) - if self.channel is None: - log.error("Cannot find talent pool channel. Cannot watch nominees.") - else: - nominations = await self.bot.api_client.get( - 'bot/nominations', - params={'active': 'true'} - ) - self.update_cache(nominations) - - async def on_member_ban(self, *_): - pass - - @group(name='nominations', aliases=('n',), invoke_without_command=True) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def bigbrother_group(self, ctx: Context): - """Nominate helpers, NSA-style.""" - - await ctx.invoke(self.bot.get_command("help"), "nominations") - - @bigbrother_group.command(name='nominated', aliases=('nominees', 'all')) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watched_command(self, ctx: Context, from_cache: bool = True): - if from_cache: - lines = tuple(f"• <@{user_id}>" for user_id in self.watched_users) - - else: - active_nominations = await self.bot.api_client.get( - 'bot/nominations', - params={'active': 'true'} - ) - self.update_cache(active_nominations) - lines = tuple( - f"• <@{entry['user']}>: {entry['reason'] or '*no reason provided*'}" - for entry in active_nominations - ) - - await LinePaginator.paginate( - lines or ("There's nothing here yet.",), - ctx, - Embed( - title="Nominated users" + " (cached)" * from_cache, - color=Color.blue() - ), - empty=False - ) - - @bigbrother_group.command(name='nominate', aliases=('n',)) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def watch_command(self, ctx: Context, user: User, *, reason: str): - """Talent pool the given `user`.""" - - active_nominations = await self.bot.api_client.get( - 'bot/nominations/' + str(user.id), - ) - if active_nominations: - active_nominations = await self.bot.api_client.put( - 'bot/nominations/' + str(user.id), - json={'active': True} - ) - await ctx.send(":ok_hand: user's watch was updated") - - else: - active_nominations = await self.bot.api_client.post( - 'bot/nominations/' + str(user.id), - json={ - 'active': True, - 'author': ctx.author.id, - 'reason': reason, - } - ) - self.watched_users.add(user.id) - await ctx.send(":ok_hand: user added to talent pool") - - @bigbrother_group.command(name='unnominate', aliases=('un',)) - @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def unwatch_command(self, ctx: Context, user: User): - """Stop talent pooling the given `user`.""" - - nomination = await self.bot.api_client.get( - 'bot/nominations/' + str(user.id) - ) - - if not nomination['active']: - await ctx.send(":x: the nomination is already inactive") - - else: - await self.bot.api_client.put( - 'bot/nominations/' + str(user.id), - json={'active': False} - ) - self.watched_users.remove(user.id) - if user.id in self.channel_queues: - del self.channel_queues[user.id] - await ctx.send(f":ok_hand: {user} is no longer part of the talent pool") - - -def setup(bot): - bot.add_cog(Nominations(bot)) - log.info("Cog loaded: Nominations") -- cgit v1.2.3 From 76ef948eb27e05b9c3f5cb00944d337c2ba903a0 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 28 Jun 2019 12:22:18 +0200 Subject: Updating constants to include talent-pool --- bot/constants.py | 1 + config-default.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 3c129afd2..82df8c6f0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -336,6 +336,7 @@ class Channels(metaclass=YAMLGetter): off_topic_3: int python: int reddit: int + talent_pool: int verification: int diff --git a/config-default.yml b/config-default.yml index 8bfc84cb4..7d9d56aff 100644 --- a/config-default.yml +++ b/config-default.yml @@ -109,6 +109,7 @@ guild: python: 267624335836053506 reddit: 458224812528238616 staff_lounge: &STAFF_LOUNGE 464905259261755392 + talent_pool: &TALENT_POOL 534321732593647616 verification: 352442727016693763 ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] -- cgit v1.2.3 From c5d4505cc4f21d87d6177cb8208d4c64d85e1052 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 28 Jun 2019 19:20:06 +0200 Subject: force child to set parameters via __init__; return value annotations; correcting spelling mistakes; + small fixes --- bot/cogs/watchchannels/bigbrother.py | 21 ++++++++------- bot/cogs/watchchannels/talentpool.py | 27 +++++++++---------- bot/cogs/watchchannels/watchchannel.py | 47 ++++++++++++++++------------------ 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 55805cf87..6e894de1e 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -16,16 +16,15 @@ log = logging.getLogger(__name__) class BigBrother(WatchChannel): """User monitoring to assist with moderation""" - def __init__(self, bot): - super().__init__(bot) - self.log = log # to ensure logs created in the super() get the name of this file - - self.destination = Channels.big_brother_logs - self.webhook_id = Webhooks.big_brother - self.api_endpoint = 'bot/infractions' - self.api_default_params = { - 'active': 'true', 'type': 'watch', 'ordering': '-inserted_at' - } + def __init__(self, bot) -> None: + super().__init__( + bot, + destination=Channels.big_brother_logs, + webhook_id=Webhooks.big_brother, + api_endpoint='bot/infractions', + api_default_params={'active': 'true', 'type': 'watch', 'ordering': '-inserted_at'}, + logger=log + ) @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -77,7 +76,7 @@ class BigBrother(WatchChannel): if response is not None: self.watched_users[user.id] = response e = Embed( - description=f":white_check_mark: **Messages sent by {user} will now be relayed**", + description=f":white_check_mark: **Messages sent by {user} will now be relayed to BigBrother**", color=Color.green() ) return await ctx.send(embed=e) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 773547bab..6773ddc89 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -18,14 +18,15 @@ STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- I class TalentPool(WatchChannel): """A TalentPool for helper nominees""" - def __init__(self, bot): - super().__init__(bot) - self.log = log # to ensure logs created in the super() get the name of this file - - self.destination = Channels.big_brother_logs - self.webhook_id = Webhooks.talent_pool - self.api_endpoint = 'bot/nominations' - self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'} + def __init__(self, bot) -> None: + super().__init__( + bot, + destination=Channels.talent_pool, + webhook_id=Webhooks.talent_pool, + api_endpoint='bot/nominations', + api_default_params={'active': 'true', 'ordering': '-inserted_at'}, + logger=log, + ) @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -83,7 +84,7 @@ class TalentPool(WatchChannel): ) return await ctx.send(embed=e) - # Manual request with `raise_for_status` as False becausse we want the actual response + # Manual request with `raise_for_status` as False because we want the actual response session = self.bot.api_client.session url = self.bot.api_client._url_for(self.api_endpoint) kwargs = { @@ -103,12 +104,12 @@ class TalentPool(WatchChannel): color=Color.red() ) return await ctx.send(embed=e) - elif resp.status >= 400: - resp.raise_for_status() + + resp.raise_for_status() self.watched_users[user.id] = response_data e = Embed( - description=f":white_check_mark: **Messages sent by {user} will now be relayed**", + description=f":white_check_mark: **Messages sent by {user} will now be relayed to TalentPool**", color=Color.green() ) return await ctx.send(embed=e) @@ -223,7 +224,7 @@ class TalentPool(WatchChannel): ) await ctx.send(embed=e) - def _nomination_to_string(self, nomination_object): + def _nomination_to_string(self, nomination_object: dict) -> str: """Creates a string representation of a nomination""" guild = self.bot.get_guild(Guild.id) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 5f9d8d1dd..566f7d52a 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -44,7 +44,7 @@ class WatchChannel(ABC): """ @abstractmethod - def __init__(self, bot: Bot) -> None: + def __init__(self, bot: Bot, destination, webhook_id, api_endpoint, api_default_params, logger) -> None: """ abstractmethod for __init__ which should still be called with super(). @@ -54,11 +54,11 @@ class WatchChannel(ABC): """ self.bot = bot - # These attributes need to be overwritten in the child class - self.destination = None # Channels.big_brother_logs - self.webhook_id = None # Webhooks.big_brother - self.api_endpoint = None # 'bot/infractions' - self.api_default_params = None # {'active': 'true', 'type': 'watch'} + self.destination = destination # E.g., Channels.big_brother_logs + self.webhook_id = webhook_id # E.g., Webhooks.big_brother + self.api_endpoint = api_endpoint # E.g., 'bot/infractions' + self.api_default_params = api_default_params # E.g., {'active': 'true', 'type': 'watch'} + self.log = logger # Logger of the child cog for a correct name in the logs # These attributes can be left as they are in the child class self._consume_task = None @@ -99,6 +99,10 @@ class WatchChannel(ABC): else: self.log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") + # Let's try again in a minute. + await asyncio.sleep(60) + self._start = self.bot.loop.create_task(self.start_watchchannel()) + async def initialize_channel(self) -> bool: """ Checks if channel and webhook are set; if not, tries to initialize them. @@ -160,7 +164,7 @@ class WatchChannel(ABC): return True - async def on_message(self, msg: Message): + async def on_message(self, msg: Message) -> None: """Queues up messages sent by watched users.""" if msg.author.id in self.watched_users: if not self.consuming_messages: @@ -169,11 +173,11 @@ class WatchChannel(ABC): self.log.trace(f"Received message: {msg.content} ({len(msg.attachments)} attachments)") self.message_queue[msg.author.id][msg.channel.id].append(msg) - async def consume_messages(self, delay_consumption: bool = True): + async def consume_messages(self, delay_consumption: bool = True) -> None: """Consumes the message queues to log watched users' messages.""" if delay_consumption: self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") - await asyncio.sleep(1) + await asyncio.sleep(BigBrotherConfig.log_delay) self.log.trace(f"{self.__class__.__name__} started consuming the message queue") @@ -203,7 +207,7 @@ class WatchChannel(ABC): async def webhook_send( self, content: Optional[str] = None, username: Optional[str] = None, avatar_url: Optional[str] = None, embed: Optional[Embed] = None, - ): + ) -> None: """Sends a message to the webhook with the specified kwargs.""" try: await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed) @@ -226,6 +230,7 @@ class WatchChannel(ABC): cleaned_content = msg.clean_content if cleaned_content: + # Put all non-media URLs in a codeblock to prevent embeds media_urls = {embed.url for embed in msg.embeds if embed.type in ("image", "video")} for url in URL_RE.findall(cleaned_content): if url not in media_urls: @@ -257,8 +262,8 @@ class WatchChannel(ABC): self.message_history[2] += 1 - async def send_header(self, msg): - """Sends an header embed to the WatchChannel""" + async def send_header(self, msg) -> None: + """Sends a header embed to the WatchChannel""" user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) @@ -266,11 +271,7 @@ class WatchChannel(ABC): actor = actor.display_name if actor else self.watched_users[user_id]['actor'] inserted_at = self.watched_users[user_id]['inserted_at'] - date_time = datetime.datetime.strptime( - inserted_at, - "%Y-%m-%dT%H:%M:%S.%fZ" - ).replace(tzinfo=None) - time_delta = time_since(date_time, precision="minutes", max_units=1) + time_delta = self._get_time_delta(inserted_at) reason = self.watched_users[user_id]['reason'] @@ -287,22 +288,21 @@ class WatchChannel(ABC): avatar_url=msg.author.avatar_url ) - async def list_watched_users(self, ctx: Context, update_cache: bool = False) -> None: + async def list_watched_users(self, ctx: Context, update_cache: bool = True) -> None: """ Gives an overview of the watched user list for this channel. The optional kwarg `update_cache` specifies whether the cache should be refreshed by polling the API. """ - if update_cache: if not await self.fetch_user_cache(): e = Embed( - description=f":x: **Failed to update {self.__class__.__name__} user cache**", + description=f":x: **Failed to update {self.__class__.__name__} user cache, serving from cache**", color=Color.red() ) await ctx.send(embed=e) - return + update_cache = False lines = [] for user_id, user_data in self.watched_users.items(): @@ -323,7 +323,6 @@ class WatchChannel(ABC): @staticmethod def _get_time_delta(time_string: str) -> str: """Returns the time in human-readable time delta format""" - date_time = datetime.datetime.strptime( time_string, "%Y-%m-%dT%H:%M:%S.%fZ" @@ -342,14 +341,12 @@ class WatchChannel(ABC): def _remove_user(self, user_id: int) -> None: """Removes user from the WatchChannel""" - self.watched_users.pop(user_id, None) self.message_queue.pop(user_id, None) self.consumption_queue.pop(user_id, None) - def cog_unload(self): + def cog_unload(self) -> None: """Takes care of unloading the cog and cancelling the consumption task.""" - self.log.trace(f"Unloading {self.__class__._name__} cog") if not self._consume_task.done(): self._consume_task.cancel() -- cgit v1.2.3 From a9734f83c44afbbe56ee70fb3bf0284b2b62f061 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 28 Jun 2019 21:02:14 +0200 Subject: Changing 'return await ctx.send' to return on next line for correct return value annotation --- bot/cogs/watchchannels/bigbrother.py | 11 +++++++---- bot/cogs/watchchannels/talentpool.py | 38 ++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 6e894de1e..1721fefb9 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -57,7 +57,8 @@ class BigBrother(WatchChannel): description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return if not await self.fetch_user_cache(): log.error("Failed to update user cache; can't watch user {user}") @@ -68,7 +69,8 @@ class BigBrother(WatchChannel): description=":x: **The specified user is already being watched**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return response = await post_infraction( ctx, user, type='watch', reason=reason, hidden=True @@ -79,7 +81,8 @@ class BigBrother(WatchChannel): description=f":white_check_mark: **Messages sent by {user} will now be relayed to BigBrother**", color=Color.green() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -112,4 +115,4 @@ class BigBrother(WatchChannel): description=":x: **The specified user is currently not being watched**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 6773ddc89..e267d4594 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -60,14 +60,16 @@ class TalentPool(WatchChannel): description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): e = Embed( description=f":x: **Nominating staff members, eh? You cheeky bastard.**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return if not await self.fetch_user_cache(): log.error(f"Failed to update user cache; can't watch user {user}") @@ -75,14 +77,16 @@ class TalentPool(WatchChannel): description=f":x: **Failed to update the user cache; can't add {user}**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return if user.id in self.watched_users: e = Embed( description=":x: **The specified user is already in the TalentPool**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return # Manual request with `raise_for_status` as False because we want the actual response session = self.bot.api_client.session @@ -103,7 +107,8 @@ class TalentPool(WatchChannel): description=":x: **The specified user can't be found in the database tables**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return resp.raise_for_status() @@ -112,7 +117,7 @@ class TalentPool(WatchChannel): description=f":white_check_mark: **Messages sent by {user} will now be relayed to TalentPool**", color=Color.green() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -130,7 +135,8 @@ class TalentPool(WatchChannel): description=":warning: **This user has never been nominated**", color=Color.blue() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return embed = Embed( title=f"Nominations for {user.display_name} `({user.id})`", @@ -167,12 +173,13 @@ class TalentPool(WatchChannel): description=":x: **The specified user does not have an active Nomination**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return [nomination] = active_nomination - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination['id']}/end", - json={'unnominate_reason': reason} + await self.bot.api_client.put( + f"{self.api_endpoint}/{nomination['id']}", + json={'end_reason': reason} ) e = Embed( description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", @@ -206,11 +213,12 @@ class TalentPool(WatchChannel): description=f":x: **Can't find a nomination with id `{nomination_id}`**", color=Color.red() ) - return await ctx.send(embed=e) + await ctx.send(embed=e) + return else: raise - field = "reason" if nomination["active"] else "unnominate_reason" + field = "reason" if nomination["active"] else "end_reason" self.log.trace(f"Changing {field} for nomination with id {nomination_id} to {reason}") await self.bot.api_client.patch( @@ -249,7 +257,7 @@ class TalentPool(WatchChannel): """ ) else: - end_date = self._get_human_readable(nomination_object["unwatched_at"]) + end_date = self._get_human_readable(nomination_object["ended_at"]) lines = textwrap.dedent( f""" =============== @@ -259,7 +267,7 @@ class TalentPool(WatchChannel): Reason: {nomination_object["reason"]} End date: {end_date} - Unwatch reason: {nomination_object["unnominate_reason"]} + Unwatch reason: {nomination_object["end_reason"]} Nomination ID: `{nomination_object["id"]}` =============== """ -- cgit v1.2.3 From 92327507bd9176e65157dc8fdf0696e2d8b26541 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 29 Jun 2019 17:21:27 +0200 Subject: Making sure a watch/unwatch reason is required when using the alias as well --- bot/cogs/alias.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index d892c7b87..f71d5d81f 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -72,9 +72,7 @@ class Alias: await self.invoke(ctx, "site resources") @command(name="watch", hidden=True) - async def bigbrother_watch_alias( - self, ctx, user: Union[Member, User, proxy_user], *, reason: str = None - ): + async def bigbrother_watch_alias(self, ctx, user: Union[Member, User, proxy_user], *, reason: str): """ Alias for invoking bigbrother watch [user] [reason]. """ @@ -82,9 +80,7 @@ class Alias: await self.invoke(ctx, "bigbrother watch", user, reason=reason) @command(name="unwatch", hidden=True) - async def bigbrother_unwatch_alias( - self, ctx, user: Union[User, proxy_user], *, reason: str = None - ): + async def bigbrother_unwatch_alias(self, ctx, user: Union[User, proxy_user], *, reason: str): """ Alias for invoking bigbrother unwatch [user] [reason]. """ @@ -178,9 +174,7 @@ class Alias: await self.invoke(ctx, "docs get", symbol) @command(name="nominate", hidden=True) - async def nomination_add_alias( - self, ctx, user: Union[Member, User, proxy_user], *, reason: str = None - ): + async def nomination_add_alias(self, ctx, user: Union[Member, User, proxy_user], *, reason: str): """ Alias for invoking talentpool add [user] [reason]. """ @@ -188,9 +182,7 @@ class Alias: await self.invoke(ctx, "talentpool add", user, reason=reason) @command(name="unnominate", hidden=True) - async def nomination_end_alias( - self, ctx, user: Union[User, proxy_user], *, reason: str = None - ): + async def nomination_end_alias(self, ctx, user: Union[User, proxy_user], *, reason: str): """ Alias for invoking nomination end [user] [reason]. """ -- cgit v1.2.3 From 7608561f06173c2ca30dadd55ebac9d42b88030f Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 29 Jun 2019 17:22:21 +0200 Subject: Applying changes requested in the reviews. Somewhat major changes include: - Reworked the start_watchchannel retry logic; - Updated docstrings; - Removed/changed Tetris-style argument lists to one-per-line or all on one line. --- bot/cogs/watchchannels/bigbrother.py | 22 +++-- bot/cogs/watchchannels/talentpool.py | 11 ++- bot/cogs/watchchannels/watchchannel.py | 173 +++++++++++++++------------------ bot/utils/messages.py | 2 +- 4 files changed, 99 insertions(+), 109 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 1721fefb9..dc5e76f55 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) class BigBrother(WatchChannel): - """User monitoring to assist with moderation""" + """Monitors users by relaying their messages to a watch channel to assist with moderation.""" def __init__(self, bot) -> None: super().__init__( @@ -29,7 +29,7 @@ class BigBrother(WatchChannel): @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def bigbrother_group(self, ctx: Context) -> None: - """Monitors users by relaying their messages to the BigBrother watch channel""" + """Monitors users by relaying their messages to the BigBrother watch channel.""" await ctx.invoke(self.bot.get_command("help"), "bigbrother") @bigbrother_group.command(name='watched', aliases=('all', 'list')) @@ -54,7 +54,7 @@ class BigBrother(WatchChannel): """ if user.bot: e = Embed( - description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", + description=f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.", color=Color.red() ) await ctx.send(embed=e) @@ -66,7 +66,7 @@ class BigBrother(WatchChannel): if user.id in self.watched_users: e = Embed( - description=":x: **The specified user is already being watched**", + description=":x: The specified user is already being watched.", color=Color.red() ) await ctx.send(embed=e) @@ -78,7 +78,7 @@ class BigBrother(WatchChannel): if response is not None: self.watched_users[user.id] = response e = Embed( - description=f":white_check_mark: **Messages sent by {user} will now be relayed to BigBrother**", + description=f":white_check_mark: Messages sent by {user} will now be relayed to BigBrother.", color=Color.green() ) await ctx.send(embed=e) @@ -97,22 +97,24 @@ class BigBrother(WatchChannel): ) if active_watches: [infraction] = active_watches + await self.bot.api_client.patch( f"{self.api_endpoint}/{infraction['id']}", json={'active': False} ) - await post_infraction( - ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False - ) + + await post_infraction(ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False) + e = Embed( - description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", + description=f":white_check_mark: Messages sent by {user} will no longer be relayed.", color=Color.green() ) await ctx.send(embed=e) + self._remove_user(user.id) else: e = Embed( - description=":x: **The specified user is currently not being watched**", + description=":x: The specified user is currently not being watched.", color=Color.red() ) await ctx.send(embed=e) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index e267d4594..5e515fe2e 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -17,7 +17,8 @@ STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- I class TalentPool(WatchChannel): - """A TalentPool for helper nominees""" + """Relays messages of helper candidates to the talent-pool channel to observe them.""" + def __init__(self, bot) -> None: super().__init__( bot, @@ -109,8 +110,8 @@ class TalentPool(WatchChannel): ) await ctx.send(embed=e) return - - resp.raise_for_status() + else: + resp.raise_for_status() self.watched_users[user.id] = response_data e = Embed( @@ -122,7 +123,7 @@ class TalentPool(WatchChannel): @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def history_command(self, ctx: Context, user: Union[User, proxy_user]) -> None: - """Shows the specified user's nomination history""" + """Shows the specified user's nomination history.""" result = await self.bot.api_client.get( self.api_endpoint, params={ @@ -233,7 +234,7 @@ class TalentPool(WatchChannel): await ctx.send(embed=e) def _nomination_to_string(self, nomination_object: dict) -> str: - """Creates a string representation of a nomination""" + """Creates a string representation of a nomination.""" guild = self.bot.get_guild(Guild.id) actor_id = nomination_object["actor"] diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 566f7d52a..8f0bc765d 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -2,6 +2,7 @@ import asyncio import datetime import logging import re +import textwrap from abc import ABC, abstractmethod from collections import defaultdict, deque from typing import Optional @@ -11,7 +12,8 @@ import discord from discord import Color, Embed, Message, Object, errors from discord.ext.commands import BadArgument, Bot, Context -from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig +from bot.cogs.modlog import ModLog +from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons from bot.pagination import LinePaginator from bot.utils import messages from bot.utils.time import time_since @@ -22,36 +24,26 @@ URL_RE = re.compile(r"(https?://[^\s]+)") def proxy_user(user_id: str) -> Object: + """A proxy user object that mocks a real User instance for when the later is not available.""" try: user_id = int(user_id) except ValueError: raise BadArgument + user = Object(user_id) user.mention = user.id user.display_name = f"<@{user.id}>" user.avatar_url_as = lambda static_format: None user.bot = False + return user class WatchChannel(ABC): - """ - Base class for WatchChannels - - Abstracts the basic functionality for watchchannels in - a granular manner to allow for easy overwritting of - methods in the child class. - """ + """ABC that implements watch channel functionality to relay all messages of a user to a watch channel.""" @abstractmethod def __init__(self, bot: Bot, destination, webhook_id, api_endpoint, api_default_params, logger) -> None: - """ - abstractmethod for __init__ which should still be called with super(). - - Note: Some of the attributes below need to be overwritten in the - __init__ of the child after the super().__init__(*args, **kwargs) - call. - """ self.bot = bot self.destination = destination # E.g., Channels.big_brother_logs @@ -73,6 +65,11 @@ class WatchChannel(ABC): self._start = self.bot.loop.create_task(self.start_watchchannel()) + @property + def modlog(self) -> ModLog: + """Provides access to the ModLog cog for alert purposes.""" + return self.bot.get_cog("ModLog") + @property def consuming_messages(self) -> bool: """Checks if a consumption task is currently running.""" @@ -83,7 +80,7 @@ class WatchChannel(ABC): exc = self._consume_task.exception() if exc: self.log.exception( - f"{self.__class__.__name__} consume task has failed with:", + f"The message queue consume task has failed with:", exc_info=exc ) return False @@ -91,52 +88,58 @@ class WatchChannel(ABC): return True async def start_watchchannel(self) -> None: - """Retrieves watched users from the API.""" + """Starts the watch channel by getting the channel, webhook, and user cache ready.""" await self.bot.wait_until_ready() - if await self.initialize_channel() and await self.fetch_user_cache(): - self.log.trace(f"Started the {self.__class__.__name__} WatchChannel") + # After updating d.py, this block can be replaced by `fetch_channel` with a try-except + for attempt in range(1, self.retries+1): + self.channel = self.bot.get_channel(self.destination) + if self.channel is None: + if attempt < self.retries: + await asyncio.sleep(self.retry_delay) + else: + break else: - self.log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") + self.log.error(f"Failed to retrieve the text channel with id `{self.destination}") - # Let's try again in a minute. - await asyncio.sleep(60) - self._start = self.bot.loop.create_task(self.start_watchchannel()) + # `get_webhook_info` has been renamed to `fetch_webhook` in newer versions of d.py + try: + self.webhook = await self.bot.get_webhook_info(self.webhook_id) + except (discord.HTTPException, discord.NotFound, discord.Forbidden): + self.log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") - async def initialize_channel(self) -> bool: - """ - Checks if channel and webhook are set; if not, tries to initialize them. + if self.channel is None or self.webhook is None: + self.log.error("Failed to start the watch channel; unloading the cog.") - Since the internal channel cache may not be available directly after `ready`, - this function will retry to get the channel a number of times. If both the - channel and webhook were initialized successfully. this function will return - `True`. - """ - if self.channel is None: - for attempt in range(1, self.retries + 1): - self.channel = self.bot.get_channel(self.destination) - - if self.channel is None: - self.log.error(f"Failed to get the {self.__class__.__name__} channel; cannot watch users") - if attempt < self.initialization_retries: - self.log.error(f"Attempt {attempt}/{self.retries}; Retrying in {self.retry_delay} seconds...") - await asyncio.sleep(self.retry_delay) - else: - self.log.trace(f"Retrieved the TextChannel for {self.__class__.__name__}") - break - else: - self.log.error(f"Cannot get channel with id `{self.destination}`; cannot watch users") - return False + message = textwrap.dedent( + f""" + An error occurred while loading the text channel or webhook. - if self.webhook is None: - self.webhook = await self.bot.get_webhook_info(self.webhook_id) # This is `fetch_webhook` in current - if self.webhook is None: - self.log.error(f"Cannot get webhook with id `{self.webhook_id}`; cannot watch users") - return False - self.log.trace(f"Retrieved the webhook for {self.__class__.__name__}") + TextChannel: {"**Failed to load**" if self.channel is None else "Loaded successfully"} + Webhook: {"**Failed to load**" if self.webhook is None else "Loaded successfully"} - self.log.trace(f"WatchChannel for {self.__class__.__name__} is fully initialized") - return True + The Cog has been unloaded.""" + ) + + await self.modlog.send_log_message( + title=f"Error: Failed to initialize the {self.__class__.__name__} watch channel", + text=message, + ping_everyone=False, + icon_url=Icons.token_removed, + colour=Color.red() + ) + + self.bot.remove_cog(self.__class__.__name__) + return + + if not await self.fetch_user_cache(): + await self.modlog.send_log_message( + title=f"Warning: Failed to retrieve user cache for the {self.__class__.__name__} watch channel", + text="Could not retrieve the list of watched users from the API and messages will not be relayed.", + ping_everyone=True, + icon=Icons.token_removed, + color=Color.red() + ) async def fetch_user_cache(self) -> bool: """ @@ -145,15 +148,9 @@ class WatchChannel(ABC): This function returns `True` if the update succeeded. """ try: - data = await self.bot.api_client.get( - self.api_endpoint, - params=self.api_default_params - ) + data = await self.bot.api_client.get(self.api_endpoint, params=self.api_default_params) except aiohttp.ClientResponseError as e: - self.log.exception( - f"Failed to fetch {self.__class__.__name__} watched users from API", - exc_info=e - ) + self.log.exception(f"Failed to fetch the watched users from the API", exc_info=e) return False self.watched_users = defaultdict(dict) @@ -181,7 +178,7 @@ class WatchChannel(ABC): self.log.trace(f"{self.__class__.__name__} started consuming the message queue") - # Prevent losing a partly processed consumption queue after Task failure + # If the previous consumption Task failed, first consume the existing comsumption_queue if not self.consumption_queue: self.consumption_queue = self.message_queue.copy() self.message_queue.clear() @@ -198,15 +195,16 @@ class WatchChannel(ABC): if self.message_queue: self.log.trace("Channel queue not empty: Continuing consuming queues") - self._consume_task = self.bot.loop.create_task( - self.consume_messages(delay_consumption=False) - ) + self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) else: self.log.trace("Done consuming messages.") async def webhook_send( - self, content: Optional[str] = None, username: Optional[str] = None, - avatar_url: Optional[str] = None, embed: Optional[Embed] = None, + self, + content: Optional[str] = None, + username: Optional[str] = None, + avatar_url: Optional[str] = None, + embed: Optional[Embed] = None, ) -> None: """Sends a message to the webhook with the specified kwargs.""" try: @@ -218,7 +216,7 @@ class WatchChannel(ABC): ) async def relay_message(self, msg: Message) -> None: - """Relays the message to the relevant WatchChannel""" + """Relays the message to the relevant watch channel""" last_author, last_channel, count = self.message_history limit = BigBrotherConfig.header_message_limit @@ -230,7 +228,7 @@ class WatchChannel(ABC): cleaned_content = msg.clean_content if cleaned_content: - # Put all non-media URLs in a codeblock to prevent embeds + # Put all non-media URLs in a code block to prevent embeds media_urls = {embed.url for embed in msg.embeds if embed.type in ("image", "video")} for url in URL_RE.findall(cleaned_content): if url not in media_urls: @@ -263,7 +261,7 @@ class WatchChannel(ABC): self.message_history[2] += 1 async def send_header(self, msg) -> None: - """Sends a header embed to the WatchChannel""" + """Sends a header embed with information about the relayed messages to the watch channel""" user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) @@ -275,18 +273,10 @@ class WatchChannel(ABC): reason = self.watched_users[user_id]['reason'] - embed = Embed(description=( - f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})\n" - )) - embed.set_footer(text=( - f"Added {time_delta} by {actor} | " - f"Reason: {reason}" - )) - await self.webhook_send( - embed=embed, - username=msg.author.display_name, - avatar_url=msg.author.avatar_url - ) + embed = Embed(description=f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})") + embed.set_footer(text=f"Added {time_delta} by {actor} | Reason: {reason}") + + await self.webhook_send(embed=embed, username=msg.author.display_name, avatar_url=msg.author.avatar_url) async def list_watched_users(self, ctx: Context, update_cache: bool = True) -> None: """ @@ -310,15 +300,12 @@ class WatchChannel(ABC): time_delta = self._get_time_delta(inserted_at) lines.append(f"• <@{user_id}> (added {time_delta})") - await LinePaginator.paginate( - lines or ("There's nothing here yet.",), - ctx, - Embed( - title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", - color=Color.blue() - ), - empty=False + lines = lines or ("There's nothing here yet.",) + embed = Embed( + title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", + color=Color.blue() ) + await LinePaginator.paginate(lines, ctx, embed, empty=False) @staticmethod def _get_time_delta(time_string: str) -> str: @@ -346,7 +333,7 @@ class WatchChannel(ABC): self.consumption_queue.pop(user_id, None) def cog_unload(self) -> None: - """Takes care of unloading the cog and cancelling the consumption task.""" + """Takes care of unloading the cog and canceling the consumption task.""" self.log.trace(f"Unloading {self.__class__._name__} cog") if not self._consume_task.done(): self._consume_task.cancel() @@ -354,6 +341,6 @@ class WatchChannel(ABC): self._consume_task.result() except asyncio.CancelledError as e: self.log.exception( - f"The {self.__class__._name__} consume task was cancelled. Messages may be lost.", + f"The {self.__class__._name__} consume task was canceled. Messages may be lost.", exc_info=e ) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index b285444c2..5c9b5b4d7 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -80,7 +80,7 @@ async def wait_for_deletion( async def send_attachments(message: Message, destination: Union[TextChannel, Webhook]): """ - Re-uploads each attachment in a message to the given channel. + Re-uploads each attachment in a message to the given channel or webhook. Each attachment is sent as a separate message to more easily comply with the 8 MiB request size limit. If attachments are too large, they are instead grouped into a single embed which links to them. -- cgit v1.2.3 From 9c96c41928f999c3c6163e4fcdcaa762fbf4b786 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 1 Jul 2019 15:39:01 +0200 Subject: Apply docstring and logging message suggestions Co-Authored-By: Mark --- bot/cogs/watchchannels/watchchannel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 8f0bc765d..020eabe45 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -40,7 +40,7 @@ def proxy_user(user_id: str) -> Object: class WatchChannel(ABC): - """ABC that implements watch channel functionality to relay all messages of a user to a watch channel.""" + """ABC with functionality for relaying users' messages to a certain channel.""" @abstractmethod def __init__(self, bot: Bot, destination, webhook_id, api_endpoint, api_default_params, logger) -> None: @@ -100,7 +100,7 @@ class WatchChannel(ABC): else: break else: - self.log.error(f"Failed to retrieve the text channel with id `{self.destination}") + self.log.error(f"Failed to retrieve the text channel with id {self.destination}") # `get_webhook_info` has been renamed to `fetch_webhook` in newer versions of d.py try: -- cgit v1.2.3 From d8fc80cefa272901e27e54564298596ab7ce4514 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 3 Jul 2019 16:31:05 +0200 Subject: Applied the following changes requested by reviews: - Bot responses are now plain messages instead of embeds; - The message history in the WatchChannel cog is now stored using a new dataclass, MessageHistory; - Removed the truncation option from the modified name field in logging; - bigbrother now provides user feedback when watching fails due to failing cache update; - Changed the lay-out of the mod_log alert and set ping_everyone to True; - Changed the function signature of `post_infraction` utility function to the one-parameter-per-line style; - Moved the declaration of File within bot/utils/messages.py to before the if/else to make things DRY. --- bot/__init__.py | 2 +- bot/cogs/watchchannels/bigbrother.py | 35 +++++-------------- bot/cogs/watchchannels/talentpool.py | 64 +++++++--------------------------- bot/cogs/watchchannels/watchchannel.py | 25 +++++++++---- bot/utils/messages.py | 6 ++-- bot/utils/moderation.py | 9 +++-- 6 files changed, 50 insertions(+), 91 deletions(-) diff --git a/bot/__init__.py b/bot/__init__.py index fecd7ceb3..b6919a489 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -55,7 +55,7 @@ else: logging.basicConfig( - format="%(asctime)s pd.beardfist.com Bot: | %(name)33.33s | %(levelname)8s | %(message)s", + format="%(asctime)s pd.beardfist.com Bot: | %(name)33s | %(levelname)8s | %(message)s", datefmt="%b %d %H:%M:%S", level=logging.TRACE if DEBUG_MODE else logging.INFO, handlers=logging_handlers diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index dc5e76f55..ff26794f7 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -53,36 +53,25 @@ class BigBrother(WatchChannel): in the header when relaying messages of this user to the watchchannel. """ if user.bot: - e = Embed( - description=f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") return if not await self.fetch_user_cache(): - log.error("Failed to update user cache; can't watch user {user}") + + await ctx.send(f":x: Updating the user cache failed, can't watch user {user}") return if user.id in self.watched_users: - e = Embed( - description=":x: The specified user is already being watched.", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(":x: The specified user is already being watched.") return response = await post_infraction( ctx, user, type='watch', reason=reason, hidden=True ) + if response is not None: self.watched_users[user.id] = response - e = Embed( - description=f":white_check_mark: Messages sent by {user} will now be relayed to BigBrother.", - color=Color.green() - ) - await ctx.send(embed=e) - return + await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to BigBrother.") @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -105,16 +94,8 @@ class BigBrother(WatchChannel): await post_infraction(ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False) - e = Embed( - description=f":white_check_mark: Messages sent by {user} will no longer be relayed.", - color=Color.green() - ) - await ctx.send(embed=e) + await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed.") self._remove_user(user.id) else: - e = Embed( - description=":x: The specified user is currently not being watched.", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(":x: The specified user is currently not being watched.") diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 5e515fe2e..45b695e55 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -57,36 +57,19 @@ class TalentPool(WatchChannel): in the header when relaying messages of this user to the watchchannel. """ if user.bot: - e = Embed( - description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") return if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): - e = Embed( - description=f":x: **Nominating staff members, eh? You cheeky bastard.**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: Nominating staff members, eh? Here's a cookie :cookie:") return if not await self.fetch_user_cache(): - log.error(f"Failed to update user cache; can't watch user {user}") - e = Embed( - description=f":x: **Failed to update the user cache; can't add {user}**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: Failed to update the user cache; can't add {user}") return if user.id in self.watched_users: - e = Embed( - description=":x: **The specified user is already in the TalentPool**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(":x: The specified user is already being watched in the TalentPool") return # Manual request with `raise_for_status` as False because we want the actual response @@ -105,7 +88,7 @@ class TalentPool(WatchChannel): if resp.status == 400 and response_data.get('user', False): e = Embed( - description=":x: **The specified user can't be found in the database tables**", + description=":x: The specified user can't be found in the database tables", color=Color.red() ) await ctx.send(embed=e) @@ -114,11 +97,7 @@ class TalentPool(WatchChannel): resp.raise_for_status() self.watched_users[user.id] = response_data - e = Embed( - description=f":white_check_mark: **Messages sent by {user} will now be relayed to TalentPool**", - color=Color.green() - ) - await ctx.send(embed=e) + await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to TalentPool") @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -132,11 +111,7 @@ class TalentPool(WatchChannel): } ) if not result: - e = Embed( - description=":warning: **This user has never been nominated**", - color=Color.blue() - ) - await ctx.send(embed=e) + await ctx.send(":warning: This user has never been nominated") return embed = Embed( @@ -170,11 +145,7 @@ class TalentPool(WatchChannel): ) if not active_nomination: - e = Embed( - description=":x: **The specified user does not have an active Nomination**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(":x: The specified user does not have an active Nomination") return [nomination] = active_nomination @@ -182,11 +153,7 @@ class TalentPool(WatchChannel): f"{self.api_endpoint}/{nomination['id']}", json={'end_reason': reason} ) - e = Embed( - description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", - color=Color.green() - ) - await ctx.send(embed=e) + await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") self._remove_user(user.id) @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) @@ -210,11 +177,7 @@ class TalentPool(WatchChannel): except ClientResponseError as e: if e.status == 404: self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") - e = Embed( - description=f":x: **Can't find a nomination with id `{nomination_id}`**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") return else: raise @@ -222,16 +185,13 @@ class TalentPool(WatchChannel): field = "reason" if nomination["active"] else "end_reason" self.log.trace(f"Changing {field} for nomination with id {nomination_id} to {reason}") + await self.bot.api_client.patch( f"{self.api_endpoint}/{nomination_id}", json={field: reason} ) - e = Embed( - description=f":white_check_mark: **Updated the {field} of the nomination!**", - color=Color.green() - ) - await ctx.send(embed=e) + await ctx.send(f":white_check_mark: Updated the {field} of the nomination!") def _nomination_to_string(self, nomination_object: dict) -> str: """Creates a string representation of a nomination.""" diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 8f0bc765d..16212e3b0 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -5,7 +5,8 @@ import re import textwrap from abc import ABC, abstractmethod from collections import defaultdict, deque -from typing import Optional +from dataclasses import dataclass +from typing import Iterator, Optional import aiohttp import discord @@ -39,6 +40,16 @@ def proxy_user(user_id: str) -> Object: return user +@dataclass +class MessageHistory: + last_author: Optional[int] = None + last_channel: Optional[int] = None + message_count: int = 0 + + def __iter__(self) -> Iterator: + return iter((self.last_author, self.last_channel, self.message_count)) + + class WatchChannel(ABC): """ABC that implements watch channel functionality to relay all messages of a user to a watch channel.""" @@ -52,7 +63,6 @@ class WatchChannel(ABC): self.api_default_params = api_default_params # E.g., {'active': 'true', 'type': 'watch'} self.log = logger # Logger of the child cog for a correct name in the logs - # These attributes can be left as they are in the child class self._consume_task = None self.watched_users = defaultdict(dict) self.message_queue = defaultdict(lambda: defaultdict(deque)) @@ -61,7 +71,7 @@ class WatchChannel(ABC): self.retry_delay = 10 self.channel = None self.webhook = None - self.message_history = [None, None, 0] + self.message_history = MessageHistory() self._start = self.bot.loop.create_task(self.start_watchchannel()) @@ -118,13 +128,14 @@ class WatchChannel(ABC): TextChannel: {"**Failed to load**" if self.channel is None else "Loaded successfully"} Webhook: {"**Failed to load**" if self.webhook is None else "Loaded successfully"} - The Cog has been unloaded.""" + The Cog has been unloaded. + """ ) await self.modlog.send_log_message( title=f"Error: Failed to initialize the {self.__class__.__name__} watch channel", text=message, - ping_everyone=False, + ping_everyone=True, icon_url=Icons.token_removed, colour=Color.red() ) @@ -221,7 +232,7 @@ class WatchChannel(ABC): limit = BigBrotherConfig.header_message_limit if msg.author.id != last_author or msg.channel.id != last_channel or count >= limit: - self.message_history = [msg.author.id, msg.channel.id, 0] + self.message_history = MessageHistory(last_author=msg.author.id, last_channel=msg.channel.id) await self.send_header(msg) @@ -258,7 +269,7 @@ class WatchChannel(ABC): exc_info=exc ) - self.message_history[2] += 1 + self.message_history.message_count += 1 async def send_header(self, msg) -> None: """Sends a header embed with information about the relayed messages to the watch channel""" diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 5c9b5b4d7..94a8b36ed 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -97,11 +97,13 @@ async def send_attachments(message: Message, destination: Union[TextChannel, Web if attachment.size <= MAX_SIZE - 512: with BytesIO() as file: await attachment.save(file) + attachment_file = File(file, filename=attachment.filename) + if isinstance(destination, TextChannel): - await destination.send(file=File(file, filename=attachment.filename)) + await destination.send(file=attachment_file) else: await destination.send( - file=File(file, filename=attachment.filename), + file=attachment_file, username=message.author.display_name, avatar_url=message.author.avatar_url ) diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index e81186253..c1eb98dd6 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -14,8 +14,13 @@ HEADERS = {"X-API-KEY": Keys.site_api} async def post_infraction( - ctx: Context, user: Union[Member, Object, User], type: str, reason: str, - expires_at: datetime = None, hidden: bool = False, active: bool = True + ctx: Context, + user: Union[Member, Object, User], + type: str, + reason: str, + expires_at: datetime = None, + hidden: bool = False, + active: bool = True, ): payload = { -- cgit v1.2.3 From 7e6d25a7f2a465c4a08d55f94ab760de27c30fb0 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 3 Jul 2019 20:04:31 +0200 Subject: Change end nomination API endpoint to PATCH endpoint --- bot/cogs/watchchannels/bigbrother.py | 1 - bot/cogs/watchchannels/talentpool.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index ff26794f7..5bf644f38 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -57,7 +57,6 @@ class BigBrother(WatchChannel): return if not await self.fetch_user_cache(): - await ctx.send(f":x: Updating the user cache failed, can't watch user {user}") return diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 45b695e55..b1be6b9d9 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -145,13 +145,13 @@ class TalentPool(WatchChannel): ) if not active_nomination: - await ctx.send(":x: The specified user does not have an active Nomination") + await ctx.send(":x: The specified user does not have an active nomination") return [nomination] = active_nomination - await self.bot.api_client.put( + await self.bot.api_client.patch( f"{self.api_endpoint}/{nomination['id']}", - json={'end_reason': reason} + json={'end_reason': reason, 'active': False} ) await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") self._remove_user(user.id) -- cgit v1.2.3 From 30ac49aefd6473d8cf037fded1b978ddc95c88cb Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 3 Jul 2019 21:27:25 +0200 Subject: Removing last embed responses and unused imports --- bot/cogs/watchchannels/bigbrother.py | 2 +- bot/cogs/watchchannels/talentpool.py | 6 +----- bot/cogs/watchchannels/watchchannel.py | 6 +----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 5bf644f38..169c3b206 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -2,7 +2,7 @@ import logging from collections import ChainMap from typing import Union -from discord import Color, Embed, User +from discord import User from discord.ext.commands import Context, group from bot.constants import Channels, Roles, Webhooks diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index b1be6b9d9..6abf7405b 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -87,11 +87,7 @@ class TalentPool(WatchChannel): response_data = await resp.json() if resp.status == 400 and response_data.get('user', False): - e = Embed( - description=":x: The specified user can't be found in the database tables", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(":x: The specified user can't be found in the database tables") return else: resp.raise_for_status() diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index c2d3d0bec..b300bb658 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -298,11 +298,7 @@ class WatchChannel(ABC): """ if update_cache: if not await self.fetch_user_cache(): - e = Embed( - description=f":x: **Failed to update {self.__class__.__name__} user cache, serving from cache**", - color=Color.red() - ) - await ctx.send(embed=e) + await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache") update_cache = False lines = [] -- cgit v1.2.3 From a4e00a3c831360ff3e7bad7e68a29db232a2c306 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 3 Jul 2019 22:01:27 +0200 Subject: Update bot/cogs/watchchannels/watchchannel.py Co-Authored-By: Mark --- 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 b300bb658..7f7efacaa 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -341,7 +341,7 @@ class WatchChannel(ABC): def cog_unload(self) -> None: """Takes care of unloading the cog and canceling the consumption task.""" - self.log.trace(f"Unloading {self.__class__._name__} cog") + self.log.trace(f"Unloading the cog") if not self._consume_task.done(): self._consume_task.cancel() try: -- cgit v1.2.3 From ffde8d451e02f1cb084e08557b52f2e5ffae6206 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 3 Jul 2019 22:04:24 +0200 Subject: Removing redundant self.__class__.__name__ occurrences Co-Authored-By: Mark --- bot/cogs/watchchannels/watchchannel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 7f7efacaa..977695134 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -187,7 +187,7 @@ class WatchChannel(ABC): self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") await asyncio.sleep(BigBrotherConfig.log_delay) - self.log.trace(f"{self.__class__.__name__} started consuming the message queue") + self.log.trace(f"Started consuming the message queue") # If the previous consumption Task failed, first consume the existing comsumption_queue if not self.consumption_queue: @@ -222,7 +222,7 @@ class WatchChannel(ABC): await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed) except discord.HTTPException as exc: self.log.exception( - f"Failed to send message to {self.__class__.__name__} webhook", + f"Failed to send a message to the webhook", exc_info=exc ) @@ -265,7 +265,7 @@ class WatchChannel(ABC): ) except discord.HTTPException as exc: self.log.exception( - f"Failed to send an attachment to {self.__class__.__name__} webhook", + f"Failed to send an attachment to the webhook", exc_info=exc ) @@ -348,6 +348,6 @@ class WatchChannel(ABC): self._consume_task.result() except asyncio.CancelledError as e: self.log.exception( - f"The {self.__class__._name__} consume task was canceled. Messages may be lost.", + f"The consume task was canceled. Messages may be lost.", exc_info=e ) -- cgit v1.2.3 From ad013b94d98987beb20f81e0c9e7142c1ffc9e78 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 11:26:32 +0200 Subject: Removing the iter/unpacking support on dataclass in favour of multiline if --- bot/cogs/watchchannels/watchchannel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index b300bb658..a0be3b1d6 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -46,9 +46,6 @@ class MessageHistory: last_channel: Optional[int] = None message_count: int = 0 - def __iter__(self) -> Iterator: - return iter((self.last_author, self.last_channel, self.message_count)) - class WatchChannel(ABC): """ABC with functionality for relaying users' messages to a certain channel.""" @@ -228,10 +225,13 @@ class WatchChannel(ABC): async def relay_message(self, msg: Message) -> None: """Relays the message to the relevant watch channel""" - last_author, last_channel, count = self.message_history limit = BigBrotherConfig.header_message_limit - if msg.author.id != last_author or msg.channel.id != last_channel or count >= limit: + if ( + msg.author.id != self.message_history.last_author + or msg.channel.id != self.message_history.last_channel + or self.message_history.count >= limit + ): self.message_history = MessageHistory(last_author=msg.author.id, last_channel=msg.channel.id) await self.send_header(msg) -- cgit v1.2.3 From c0a29b4ea1b3ddc6db119987a883dcbf8c9aa916 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 11:31:58 +0200 Subject: Fixing bug with misnamed MessageHistory attribute message_count in if-statement --- 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 46b20b4f0..9f67367fc 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -230,7 +230,7 @@ class WatchChannel(ABC): if ( msg.author.id != self.message_history.last_author or msg.channel.id != self.message_history.last_channel - or self.message_history.count >= limit + or self.message_history.message_count >= limit ): self.message_history = MessageHistory(last_author=msg.author.id, last_channel=msg.channel.id) -- cgit v1.2.3 From 0a25926f267188e4079b1575d883b9a47c6a5d10 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 13:01:14 +0200 Subject: Removing unused import --- 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 9f67367fc..87a219a96 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -6,7 +6,7 @@ import textwrap from abc import ABC, abstractmethod from collections import defaultdict, deque from dataclasses import dataclass -from typing import Iterator, Optional +from typing import Optional import aiohttp import discord -- cgit v1.2.3 From 722790f4fc946923123ba042eb1d4f41ecfceb83 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 14:13:43 +0200 Subject: Replacing BigBrother by TalentPool in TalentPool docstrings --- bot/cogs/watchchannels/bigbrother.py | 2 +- bot/cogs/watchchannels/talentpool.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 169c3b206..20cc66012 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -47,7 +47,7 @@ class BigBrother(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def watch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: """ - Relay messages sent by the given `user` to the `#big-brother-logs` channel. + Relay messages sent by the given `user` to the `#big-brother` channel. A `reason` for adding the user to BigBrother is required and will displayed in the header when relaying messages of this user to the watchchannel. diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 6abf7405b..d03ce0a7a 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -40,7 +40,7 @@ class TalentPool(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def watched_command(self, ctx: Context, update_cache: bool = True) -> None: """ - Shows the users that are currently being monitored in BigBrother. + Shows the users that are currently being monitored in TalentPool. The optional kwarg `update_cache` can be used to update the user cache using the API before listing the users. @@ -51,9 +51,9 @@ class TalentPool(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def watch_command(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: """ - Relay messages sent by the given `user` to the `#big-brother-logs` channel. + Relay messages sent by the given `user` to the `#talent-pool` channel. - A `reason` for adding the user to BigBrother is required and will displayed + A `reason` for adding the user to TalentPool is required and will displayed in the header when relaying messages of this user to the watchchannel. """ if user.bot: -- cgit v1.2.3 From f089c963b39dabfa9ff2e776f7edc6e329a4bc54 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 21:54:10 +0200 Subject: Applying docstring suggestions Co-Authored-By: Mark --- bot/cogs/watchchannels/bigbrother.py | 8 ++++---- bot/cogs/watchchannels/talentpool.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 20cc66012..e7b3d70bc 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -29,14 +29,14 @@ class BigBrother(WatchChannel): @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def bigbrother_group(self, ctx: Context) -> None: - """Monitors users by relaying their messages to the BigBrother watch channel.""" + """Monitors users by relaying their messages to the Big Brother watch channel.""" await ctx.invoke(self.bot.get_command("help"), "bigbrother") @bigbrother_group.command(name='watched', aliases=('all', 'list')) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def watched_command(self, ctx: Context, update_cache: bool = True) -> None: """ - Shows the users that are currently being monitored in BigBrother. + Shows the users that are currently being monitored by Big Brother. The optional kwarg `update_cache` can be used to update the user cache using the API before listing the users. @@ -49,7 +49,7 @@ class BigBrother(WatchChannel): """ Relay messages sent by the given `user` to the `#big-brother` channel. - A `reason` for adding the user to BigBrother is required and will displayed + A `reason` for adding the user to Big Brother is required and will be displayed in the header when relaying messages of this user to the watchchannel. """ if user.bot: @@ -70,7 +70,7 @@ class BigBrother(WatchChannel): 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 BigBrother.") + await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to Big Brother.") @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 d03ce0a7a..6f06ebc36 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -32,7 +32,7 @@ class TalentPool(WatchChannel): @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def nomination_group(self, ctx: Context) -> None: - """Highlights the activity of helper nominees by relaying their messages to TalentPool.""" + """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" await ctx.invoke(self.bot.get_command("help"), "talentpool") @@ -40,7 +40,7 @@ class TalentPool(WatchChannel): @with_role(Roles.owner, Roles.admin, Roles.moderator) async def watched_command(self, ctx: Context, update_cache: bool = True) -> None: """ - Shows the users that are currently being monitored in TalentPool. + Shows the users that are currently being monitored in the talent pool. The optional kwarg `update_cache` can be used to update the user cache using the API before listing the users. @@ -53,7 +53,7 @@ class TalentPool(WatchChannel): """ Relay messages sent by the given `user` to the `#talent-pool` channel. - A `reason` for adding the user to TalentPool is required and will displayed + A `reason` for adding the user to the talent pool is required and will be displayed in the header when relaying messages of this user to the watchchannel. """ if user.bot: @@ -69,7 +69,7 @@ class TalentPool(WatchChannel): return if user.id in self.watched_users: - await ctx.send(":x: The specified user is already being watched in the TalentPool") + await ctx.send(":x: The specified user is already being watched in the talent pool") return # Manual request with `raise_for_status` as False because we want the actual response -- cgit v1.2.3 From a395ba657c582bee03beb334619f376ba4a43134 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 22:01:15 +0200 Subject: Adding correct docstring to TalentPool edit group method; adding periods to docstrings in the WatchChannel ABC --- bot/cogs/watchchannels/talentpool.py | 2 +- bot/cogs/watchchannels/watchchannel.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 6f06ebc36..d90bd2cbd 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -155,7 +155,7 @@ class TalentPool(WatchChannel): @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def nomination_edit_group(self, ctx: Context) -> None: - """Highlights the activity of helper nominees by relaying their messages to TalentPool.""" + """Commands to edit nominations.""" await ctx.invoke(self.bot.get_command("help"), "talentpool", "edit") diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 87a219a96..fe6d6bb6e 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -224,7 +224,7 @@ class WatchChannel(ABC): ) async def relay_message(self, msg: Message) -> None: - """Relays the message to the relevant watch channel""" + """Relays the message to the relevant watch channel.""" limit = BigBrotherConfig.header_message_limit if ( @@ -272,7 +272,7 @@ class WatchChannel(ABC): self.message_history.message_count += 1 async def send_header(self, msg) -> None: - """Sends a header embed with information about the relayed messages to the watch channel""" + """Sends a header embed with information about the relayed messages to the watch channel.""" user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) @@ -316,7 +316,7 @@ class WatchChannel(ABC): @staticmethod def _get_time_delta(time_string: str) -> str: - """Returns the time in human-readable time delta format""" + """Returns the time in human-readable time delta format.""" date_time = datetime.datetime.strptime( time_string, "%Y-%m-%dT%H:%M:%S.%fZ" @@ -334,7 +334,7 @@ class WatchChannel(ABC): return date_time.strftime(output_format) def _remove_user(self, user_id: int) -> None: - """Removes user from the WatchChannel""" + """Removes a user from a watch channel.""" self.watched_users.pop(user_id, None) self.message_queue.pop(user_id, None) self.consumption_queue.pop(user_id, None) -- cgit v1.2.3 From 20683f67f336579fec9be8050ee2c8e3a40ae537 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 4 Jul 2019 22:19:33 +0200 Subject: Changing class-level docstring of TalentPool class to be consistent with the BigBrother class --- bot/cogs/watchchannels/talentpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index d90bd2cbd..75954dd4a 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -17,7 +17,7 @@ STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- I class TalentPool(WatchChannel): - """Relays messages of helper candidates to the talent-pool channel to observe them.""" + """Relays messages of helper candidates to a watch channel to observe them.""" def __init__(self, bot) -> None: super().__init__( -- cgit v1.2.3 From 443a6d7d77ff8fb30abdc8bdd1983bcc800e392a Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 5 Jul 2019 17:27:19 +0200 Subject: Apply suggestions from code review Co-Authored-By: Mark --- bot/cogs/watchchannels/talentpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 75954dd4a..6fbe2bc03 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -54,7 +54,7 @@ class TalentPool(WatchChannel): Relay messages sent by the given `user` to the `#talent-pool` channel. A `reason` for adding the user to the talent pool is required and will be displayed - in the header when relaying messages of this user to the watchchannel. + in the header when relaying messages of this user to the channel. """ if user.bot: await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") @@ -93,7 +93,7 @@ class TalentPool(WatchChannel): 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 TalentPool") + await ctx.send(f":white_check_mark: Messages sent by {user} will now be relayed to the talent pool channel") @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) -- cgit v1.2.3 From c69c6d5548063c7aa95a69478189aaccffa63917 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 6 Jul 2019 14:36:26 +0200 Subject: moving over the communities to whitelist from master. --- config-default.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config-default.yml b/config-default.yml index 7854b5db9..dff8ed599 100644 --- a/config-default.yml +++ b/config-default.yml @@ -146,6 +146,13 @@ filter: - 267624335836053506 # Python Discord - 440186186024222721 # Python Discord: ModLog Emojis - 273944235143593984 # STEM + - 348658686962696195 # RLBot + - 531221516914917387 # Pallets + - 249111029668249601 # Gentoo + - 327254708534116352 # Adafruit + - 544525886180032552 # kennethreitz.org + - 590806733924859943 # Discord Hack Week + - 423249981340778496 # Kivy domain_blacklist: - pornhub.com -- cgit v1.2.3