aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py2
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py130
-rw-r--r--bot/exts/recruitment/talentpool/_review.py20
-rw-r--r--bot/resources/tags/async-await.md28
-rw-r--r--config-default.yml3
5 files changed, 115 insertions, 68 deletions
diff --git a/bot/constants.py b/bot/constants.py
index ab55da482..7616ea4df 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -465,7 +465,6 @@ class Channels(metaclass=YAMLGetter):
voice_chat: int
big_brother_logs: int
- talent_pool: int
class Webhooks(metaclass=YAMLGetter):
@@ -476,7 +475,6 @@ class Webhooks(metaclass=YAMLGetter):
dev_log: int
duck_pond: int
incidents_archive: int
- talent_pool: int
class Roles(metaclass=YAMLGetter):
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index 03326cab2..a5b6de00f 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -1,6 +1,6 @@
import logging
import textwrap
-from collections import ChainMap
+from collections import ChainMap, defaultdict
from io import StringIO
from typing import Union
@@ -10,60 +10,74 @@ from discord.ext.commands import Cog, Context, group, has_any_role
from bot.api import ResponseCodeError
from bot.bot import Bot
-from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks
+from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, STAFF_ROLES
from bot.converters import FetchedMember
-from bot.exts.moderation.watchchannels._watchchannel import WatchChannel
from bot.exts.recruitment.talentpool._review import Reviewer
from bot.pagination import LinePaginator
from bot.utils import time
+from bot.utils.time import get_time_delta
REASON_MAX_CHARS = 1000
log = logging.getLogger(__name__)
-class TalentPool(WatchChannel, Cog, name="Talentpool"):
+class TalentPool(Cog, name="Talentpool"):
"""Relays messages of helper candidates to a watch channel to observe them."""
def __init__(self, bot: 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,
- disable_header=True,
- )
-
+ self.bot = bot
self.reviewer = Reviewer(self.__class__.__name__, bot, self)
self.bot.loop.create_task(self.reviewer.reschedule_reviews())
+ # Stores talentpool users in cache
+ self.cache = defaultdict(dict)
+ self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'}
+
+ async def refresh_cache(self) -> bool:
+ """Updates TalentPool users cache."""
+ try:
+ data = await self.bot.api_client.get(
+ 'bot/nominations',
+ params=self.api_default_params
+ )
+ except ResponseCodeError as err:
+ log.exception("Failed to fetch the watched users from the API", exc_info=err)
+ return False
+
+ self.cache = defaultdict(dict)
+
+ for entry in data:
+ user_id = entry.pop('user')
+ self.cache[user_id] = entry
+
+ return True
+
@group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def nomination_group(self, ctx: Context) -> None:
"""Highlights the activity of helper nominees by relaying their messages to the talent pool channel."""
await ctx.send_help(ctx.command)
- @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",))
+ @nomination_group.command(name='list', aliases=('all', 'watched'), root_aliases=("nominees",))
@has_any_role(*MODERATION_ROLES)
- async def watched_command(
+ async def list_command(
self,
ctx: Context,
oldest_first: bool = False,
update_cache: bool = True
) -> None:
"""
- Shows the users that are currently being monitored in the talent pool.
+ Shows the users that are currently in the talent pool.
The optional kwarg `oldest_first` can be used to order the list by oldest nomination.
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, oldest_first=oldest_first, update_cache=update_cache)
+ await self.list_users(ctx, oldest_first=oldest_first, update_cache=update_cache)
- async def list_watched_users(
+ async def list_users(
self,
ctx: Context,
oldest_first: bool = False,
@@ -80,16 +94,27 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
The optional kwarg `update_cache` specifies whether the cache should
be refreshed by polling the API.
"""
- # TODO Once the watch channel is removed, this can be done in a smarter way, without splitting and overriding
- # the list_watched_users function.
- watched_data = await self.prepare_watched_users_data(ctx, oldest_first, update_cache)
+ successful_update = False
+ if update_cache:
+ if not (successful_update := await self.refresh_cache()):
+ await ctx.send(":warning: Unable to update cache. Data may be inaccurate.")
- if update_cache and not watched_data["updated"]:
- await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache")
+ nominations = self.cache.items()
+ if oldest_first:
+ nominations = reversed(nominations)
lines = []
- for user_id, line in watched_data["info"].items():
- if self.watched_users[user_id]['reviewed']:
+
+ for user_id, user_data in nominations:
+ member = ctx.guild.get_member(user_id)
+ line = f"• `{user_id}`"
+ if member:
+ line += f" ({member.name}#{member.discriminator})"
+ inserted_at = user_data['inserted_at']
+ line += f", added {get_time_delta(inserted_at)}"
+ if not member: # Cross off users who left the server.
+ line = f"~~{line}~~"
+ if user_data['reviewed']:
line += " *(reviewed)*"
elif user_id in self.reviewer:
line += " *(scheduled)*"
@@ -99,7 +124,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
lines = ("There's nothing here yet.",)
embed = Embed(
- title=watched_data["title"],
+ title=f"Talent Pool active nominations ({'updated' if update_cache and successful_update else 'cached'})",
color=Color.blue()
)
await LinePaginator.paginate(lines, ctx, embed, empty=False)
@@ -108,12 +133,12 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@has_any_role(*MODERATION_ROLES)
async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None:
"""
- Shows talent pool monitored users ordered by oldest nomination.
+ Shows talent pool users ordered by oldest nomination.
The optional kwarg `update_cache` can be used to update the user
cache using the API before listing the users.
"""
- await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache)
+ await ctx.invoke(self.list_command, oldest_first=True, update_cache=update_cache)
@nomination_group.command(name='forcewatch', aliases=('fw', 'forceadd', 'fa'), root_aliases=("forcenominate",))
@has_any_role(*MODERATION_ROLES)
@@ -127,7 +152,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",))
@has_any_role(*STAFF_ROLES)
- async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None:
+ async def add_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None:
"""
Adds the given `user` to the talent pool.
@@ -156,8 +181,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:")
return
- if not await self.fetch_user_cache():
- await ctx.send(f":x: Failed to update the user cache; can't add {user}")
+ if not await self.refresh_cache():
+ await ctx.send(f":x: Failed to update the cache; can't add {user}")
return
if len(reason) > REASON_MAX_CHARS:
@@ -166,7 +191,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
# 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)
+ url = self.bot.api_client._url_for('bot/nominations')
kwargs = {
'json': {
'actor': ctx.author.id,
@@ -188,13 +213,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
else:
resp.raise_for_status()
- self.watched_users[user.id] = response_data
+ self.cache[user.id] = response_data
if user.id not in self.reviewer:
self.reviewer.schedule_review(user.id)
history = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params={
"user__id": str(user.id),
"active": "false",
@@ -213,7 +238,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
async def history_command(self, ctx: Context, user: FetchedMember) -> None:
"""Shows the specified user's nomination history."""
result = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params={
'user__id': str(user.id),
'ordering': "-active,-inserted_at"
@@ -237,20 +262,20 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
max_size=1000
)
- @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",))
+ @nomination_group.command(name='end', aliases=('unwatch',), root_aliases=("unnominate",))
@has_any_role(*MODERATION_ROLES)
- async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:
+ async def end_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:
"""
Ends the active nomination of the specified user with the given reason.
Providing a `reason` is required.
"""
if len(reason) > REASON_MAX_CHARS:
- await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.")
+ await ctx.send(f":x: Maximum allowed characters for the end reason is {REASON_MAX_CHARS}.")
return
if await self.unwatch(user.id, reason):
- await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed")
+ await ctx.send(f":white_check_mark: Successfully un-nominated {user}")
else:
await ctx.send(":x: The specified user does not have an active nomination")
@@ -269,10 +294,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
return
try:
- nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}")
+ nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
except ResponseCodeError as e:
if e.response.status == 404:
- self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
+ log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`")
return
else:
@@ -286,13 +311,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
await ctx.send(f":x: {actor} doesn't have an entry in this nomination.")
return
- self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}")
+ log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}")
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination_id}",
+ f"bot/nominations/{nomination_id}",
json={"actor": actor.id, "reason": reason}
)
- await self.fetch_user_cache() # Update cache
+ await self.refresh_cache() # Update cache
await ctx.send(":white_check_mark: Successfully updated nomination reason.")
@nomination_edit_group.command(name='end_reason')
@@ -304,10 +329,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
return
try:
- nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}")
+ nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
except ResponseCodeError as e:
if e.response.status == 404:
- self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
+ log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`")
return
else:
@@ -317,13 +342,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
await ctx.send(":x: Can't edit the end reason of an active nomination.")
return
- self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}")
+ log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}")
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination_id}",
+ f"bot/nominations/{nomination_id}",
json={"end_reason": reason}
)
- await self.fetch_user_cache() # Update cache.
+ await self.refresh_cache() # Update cache.
await ctx.send(":white_check_mark: Updated the end reason of the nomination!")
@nomination_group.command(aliases=('mr',))
@@ -383,7 +408,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
async def unwatch(self, user_id: int, reason: str) -> bool:
"""End the active nomination of a user with the given reason and return True on success."""
active_nomination = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params=ChainMap(
{"user__id": str(user_id)},
self.api_default_params,
@@ -398,10 +423,9 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
nomination = active_nomination[0]
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination['id']}",
+ f"bot/nominations/{nomination['id']}",
json={'end_reason': reason, 'active': False}
)
- self._remove_user(user_id)
self.reviewer.cancel(user_id)
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index b9ff61986..585640699 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -55,9 +55,9 @@ class Reviewer:
log.trace("Rescheduling reviews")
await self.bot.wait_until_guild_available()
# TODO Once the watch channel is removed, this can be done in a smarter way, e.g create a sync function.
- await self._pool.fetch_user_cache()
+ await self._pool.refresh_cache()
- for user_id, user_data in self._pool.watched_users.items():
+ for user_id, user_data in self._pool.cache.items():
if not user_data["reviewed"]:
self.schedule_review(user_id)
@@ -65,7 +65,7 @@ class Reviewer:
"""Schedules a single user for review."""
log.trace(f"Scheduling review of user with ID {user_id}")
- user_data = self._pool.watched_users.get(user_id)
+ user_data = self._pool.cache[user_id]
inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None)
review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL)
@@ -93,7 +93,7 @@ class Reviewer:
await last_message.add_reaction(reaction)
if update_database:
- nomination = self._pool.watched_users.get(user_id)
+ nomination = self._pool.cache[user_id]
await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True})
async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji]]:
@@ -104,7 +104,7 @@ class Reviewer:
# not to accidentally insert the IDs of users that have no
# active nominated by using the `watched_users.get(user_id)`
# instead of `watched_users[user_id]`.
- nomination = self._pool.watched_users.get(user_id)
+ nomination = self._pool.cache[user_id]
if not nomination:
log.trace(f"There doesn't appear to be an active nomination for {user_id}")
return "", None
@@ -329,7 +329,7 @@ class Reviewer:
"""
log.trace(f"Fetching the nomination history data for {member.id}'s review")
history = await self.bot.api_client.get(
- self._pool.api_endpoint,
+ "bot/nominations",
params={
"user__id": str(member.id),
"active": "false",
@@ -387,18 +387,18 @@ class Reviewer:
Returns True if the user was successfully marked as reviewed, False otherwise.
"""
log.trace(f"Updating user {user_id} as reviewed")
- await self._pool.fetch_user_cache()
- if user_id not in self._pool.watched_users:
+ await self._pool.refresh_cache()
+ if user_id not in self._pool.cache:
log.trace(f"Can't find a nominated user with id {user_id}")
await ctx.send(f":x: Can't find a currently nominated user with id `{user_id}`")
return False
- nomination = self._pool.watched_users.get(user_id)
+ nomination = self._pool.cache[user_id]
if nomination["reviewed"]:
await ctx.send(":x: This nomination was already reviewed, but here's a cookie :cookie:")
return False
- await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True})
+ await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True})
if user_id in self._review_scheduler:
self._review_scheduler.cancel(user_id)
diff --git a/bot/resources/tags/async-await.md b/bot/resources/tags/async-await.md
new file mode 100644
index 000000000..ff71ace07
--- /dev/null
+++ b/bot/resources/tags/async-await.md
@@ -0,0 +1,28 @@
+**Concurrency in Python**
+
+Python provides the ability to run multiple tasks and coroutines simultaneously with the use of the `asyncio` library, which is included in the Python standard library.
+
+This works by running these coroutines in an event loop, where the context of which coroutine is being run is switches periodically to allow all of them to run, giving the appearance of running at the same time. This is different to using threads or processes in that all code is run in the main process and thread, although it is possible to run coroutines in threads.
+
+To call an async function we can either `await` it, or run it in an event loop which we get from `asyncio`.
+
+To create a coroutine that can be used with asyncio we need to define a function using the async keyword:
+```py
+async def main():
+ await something_awaitable()
+```
+Which means we can call `await something_awaitable()` directly from within the function. If this were a non-async function this would have raised an exception like: `SyntaxError: 'await' outside async function`
+
+To run the top level async function from outside of the event loop we can get an event loop from `asyncio`, and then use that loop to run the function:
+```py
+from asyncio import get_event_loop
+
+async def main():
+ await something_awaitable()
+
+loop = get_event_loop()
+loop.run_until_complete(main())
+```
+Note that in the `run_until_complete()` where we appear to be calling `main()`, this does not execute the code in `main`, rather it returns a `coroutine` object which is then handled and run by the event loop via `run_until_complete()`.
+
+To learn more about asyncio and its use, see the [asyncio documentation](https://docs.python.org/3/library/asyncio.html).
diff --git a/config-default.yml b/config-default.yml
index 55388247c..3afe9ba3c 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -221,7 +221,6 @@ guild:
# Watch
big_brother_logs: &BB_LOGS 468507907357409333
- talent_pool: &TALENT_POOL 534321732593647616
moderation_categories:
- *MODS_CATEGORY
@@ -291,7 +290,6 @@ guild:
duck_pond: 637821475327311927
incidents_archive: 720671599790915702
python_news: &PYNEWS_WEBHOOK 704381182279942324
- talent_pool: 569145364800602132
filter:
@@ -322,7 +320,6 @@ filter:
- *MESSAGE_LOG
- *MOD_LOG
- *STAFF_LOUNGE
- - *TALENT_POOL
role_whitelist:
- *ADMINS_ROLE