diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/constants.py | 2 | ||||
| -rw-r--r-- | bot/exts/recruitment/talentpool/_cog.py | 164 | ||||
| -rw-r--r-- | bot/exts/recruitment/talentpool/_review.py | 28 | ||||
| -rw-r--r-- | config-default.yml | 3 | 
4 files changed, 111 insertions, 86 deletions
| diff --git a/bot/constants.py b/bot/constants.py index 4e99df7f3..f99913b17 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -472,7 +472,6 @@ class Channels(metaclass=YAMLGetter):      voice_chat_1: int      big_brother_logs: int -    talent_pool: int  class Webhooks(metaclass=YAMLGetter): @@ -483,7 +482,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 c297f70c2..a317c6645 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 @@ -11,12 +11,12 @@ 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, Roles, STAFF_ROLES, Webhooks +from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES  from bot.converters import MemberOrUser -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  AUTOREVIEW_ENABLED_KEY = "autoreview_enabled"  REASON_MAX_CHARS = 1000 @@ -24,25 +24,17 @@ REASON_MAX_CHARS = 1000  log = logging.getLogger(__name__) -class TalentPool(WatchChannel, Cog, name="Talentpool"): -    """Relays messages of helper candidates to a watch channel to observe them.""" +class TalentPool(Cog, name="Talentpool"): +    """Used to nominate potential helper candidates."""      # RedisCache[str, bool]      # Can contain a single key, "autoreview_enabled", with the value a bool indicating if autoreview is enabled.      talentpool_settings = RedisCache()      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.api_default_params = {'active': 'true', 'ordering': '-inserted_at'}          self.bot.loop.create_task(self.schedule_autoreviews())      async def schedule_autoreviews(self) -> None: @@ -50,12 +42,31 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):          if await self.autoreview_enabled():              await self.reviewer.reschedule_reviews()          else: -            self.log.trace("Not scheduling reviews as autoreview is disabled.") +            log.trace("Not scheduling reviews as autoreview is disabled.")      async def autoreview_enabled(self) -> bool:          """Return whether automatic posting of nomination reviews is enabled."""          return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True) +    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 currently nominated 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: @@ -106,25 +117,29 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):          else:              await ctx.send("Autoreview is currently disabled") -    @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) +    @nomination_group.command( +        name="nominees", +        aliases=("nominated", "all", "list", "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_nominated_users(ctx, oldest_first=oldest_first, update_cache=update_cache) -    async def list_watched_users( +    async def list_nominated_users(          self,          ctx: Context,          oldest_first: bool = False, @@ -141,16 +156,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)*" @@ -160,7 +186,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) @@ -169,26 +195,30 @@ 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",)) +    @nomination_group.command( +        name="forcenominate", +        aliases=("fw", "forceadd", "fa", "fn", "forcewatch"), +        root_aliases=("forcenominate",) +    )      @has_any_role(*MODERATION_ROLES) -    async def force_watch_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None: +    async def force_nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:          """          Adds the given `user` to the talent pool, from any channel.          A `reason` for adding the user to the talent pool is optional.          """ -        await self._watch_user(ctx, user, reason) +        await self._nominate_user(ctx, user, reason) -    @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) +    @nomination_group.command(name='nominate', aliases=("w", "add", "a", "watch"), root_aliases=("nominate",))      @has_any_role(*STAFF_ROLES) -    async def watch_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None: +    async def nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:          """          Adds the given `user` to the talent pool. @@ -199,26 +229,26 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):              if any(role.id in MODERATION_ROLES for role in ctx.author.roles):                  await ctx.send(                      f":x: Nominations should be run in the <#{Channels.nominations}> channel. " -                    "Use `!tp forcewatch` to override this check." +                    "Use `!tp forcenominate` to override this check."                  )              else:                  await ctx.send(f":x: Nominations must be run in the <#{Channels.nominations}> channel")              return -        await self._watch_user(ctx, user, reason) +        await self._nominate_user(ctx, user, reason) -    async def _watch_user(self, ctx: Context, user: MemberOrUser, reason: str) -> None: +    async def _nominate_user(self, ctx: Context, user: MemberOrUser, reason: str) -> None:          """Adds the given user to the talent pool."""          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.") +            await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. Only humans can be nominated.")              return          if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles):              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: @@ -227,7 +257,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, @@ -249,13 +279,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 await self.autoreview_enabled() and 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", @@ -274,7 +304,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):      async def history_command(self, ctx: Context, user: MemberOrUser) -> 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" @@ -298,20 +328,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", "unnominate"), root_aliases=("unnominate",))      @has_any_role(*MODERATION_ROLES) -    async def unwatch_command(self, ctx: Context, user: MemberOrUser, *, reason: str) -> None: +    async def end_nomination_command(self, ctx: Context, user: MemberOrUser, *, 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.mention} will no longer be relayed") +        if await self.end_nomination(user.id, reason): +            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") @@ -330,10 +360,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: @@ -347,13 +377,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):              await ctx.send(f":x: {actor.mention} 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') @@ -365,10 +395,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: @@ -378,13 +408,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',)) @@ -419,7 +449,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):      @Cog.listener()      async def on_member_ban(self, guild: Guild, user: Union[MemberOrUser]) -> None:          """Remove `user` from the talent pool after they are banned.""" -        await self.unwatch(user.id, "User was banned.") +        await self.end_nomination(user.id, "User was banned.")      @Cog.listener()      async def on_raw_reaction_add(self, payload: RawReactionActionEvent) -> None: @@ -441,10 +471,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):              log.info(f"Archiving nomination {message.id}")              await self.reviewer.archive_vote(message, emoji == Emojis.incident_actioned) -    async def unwatch(self, user_id: int, reason: str) -> bool: +    async def end_nomination(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, @@ -459,11 +489,11 @@ 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.cache.pop(user_id)          if await self.autoreview_enabled():              self.reviewer.cancel(user_id) @@ -512,7 +542,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):                  {entries_string}                  End date: {end_date} -                Unwatch reason: {nomination_object["end_reason"]} +                Unnomination reason: {nomination_object["end_reason"]}                  ===============                  """              ) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 4d496a1f7..3ffbf93f3 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -58,9 +58,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) @@ -68,7 +68,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.get(user_id)          inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None)          review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL) @@ -96,18 +96,18 @@ class Reviewer:                  await last_message.add_reaction(reaction)          if update_database: -            nomination = self._pool.watched_users.get(user_id) -            await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) +            nomination = self._pool.cache.get(user_id) +            await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True})      async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji]]:          """Format a generic review of a user and return it with the reviewed emoji."""          log.trace(f"Formatting the review of {user_id}") -        # Since `watched_users` is a defaultdict, we should take care +        # Since `cache` is a defaultdict, we should take care          # 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) +        # active nominated by using the `cache.get(user_id)` +        # instead of `cache[user_id]`. +        nomination = self._pool.cache.get(user_id)          if not nomination:              log.trace(f"There doesn't appear to be an active nomination for {user_id}")              return "", None @@ -332,7 +332,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", @@ -390,18 +390,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.get(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/config-default.yml b/config-default.yml index baece5c51..a18fdafa5 100644 --- a/config-default.yml +++ b/config-default.yml @@ -232,7 +232,6 @@ guild:          # Watch          big_brother_logs:   &BB_LOGS        468507907357409333 -        talent_pool:        &TALENT_POOL    534321732593647616      moderation_categories:          - *MODS_CATEGORY @@ -305,7 +304,6 @@ guild:          duck_pond:                          637821475327311927          incidents_archive:                  720671599790915702          python_news:        &PYNEWS_WEBHOOK 704381182279942324 -        talent_pool:                        569145364800602132  filter: @@ -336,7 +334,6 @@ filter:          - *MESSAGE_LOG          - *MOD_LOG          - *STAFF_LOUNGE -        - *TALENT_POOL      role_whitelist:          - *ADMINS_ROLE | 
