aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py9
-rw-r--r--bot/exts/recruitment/talentpool/_review.py132
2 files changed, 67 insertions, 74 deletions
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index efd92b878..ecd966c4f 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -800,9 +800,12 @@ class TalentPool(Cog, name="Talentpool"):
await ctx.send(f":x: There doesn't appear to be an active nomination for {user_id}")
return
- review, _, _ = await self.reviewer.make_review(nominations[0])
- file = discord.File(StringIO(review), f"{user_id}_review.md")
- await ctx.send(file=file)
+ review, _, _, nominations = await self.reviewer.make_review(nominations[0])
+
+ review_file = discord.File(StringIO(review), f"{user_id}_review.md")
+ nominations_file = discord.File(StringIO("\n\n".join(nominations)), f"{user_id}_nominations.md")
+
+ await ctx.send(files=[review_file, nominations_file])
@nomination_group.command(aliases=("review",))
@has_any_role(*MODERATION_ROLES)
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index 0d43de4d2..1535aaa45 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -1,22 +1,20 @@
-import asyncio
import contextlib
import random
import re
-import textwrap
import typing
from collections import Counter
from datetime import UTC, datetime, timedelta
import discord
from async_rediscache import RedisCache
-from discord import Embed, Emoji, Member, Message, NotFound, PartialMessage, TextChannel
+from discord import Embed, Emoji, Member, NotFound, PartialMessage
from pydis_core.site_api import ResponseCodeError
from pydis_core.utils.channel import get_or_fetch_channel
from pydis_core.utils.members import get_or_fetch_member
from bot.bot import Bot
from bot.constants import Channels, Colours, Emojis, Guild, Roles
-from bot.exts.recruitment.talentpool._api import Nomination, NominationAPI
+from bot.exts.recruitment.talentpool._api import Nomination, NominationAPI, NominationEntry
from bot.log import get_logger
from bot.utils import time
from bot.utils.messages import count_unique_users_reaction
@@ -46,7 +44,7 @@ RECENT_ACTIVITY_DAYS = 7
# The higher this is, the lower the effect of review age. At 1, age and number of entries are weighted equally.
REVIEW_SCORE_WEIGHT = 1.5
-# Regex for finding the first message of a nomination, and extracting the nominee.
+# Regex for finding a nomination, and extracting the nominee.
NOMINATION_MESSAGE_REGEX = re.compile(
r"<@!?(\d+)> \(.+(#\d{4})?\) for Helper!\n\n",
re.MULTILINE
@@ -230,7 +228,7 @@ class Reviewer:
async def post_review(self, nomination: Nomination) -> None:
"""Format the review of a user and post it to the nomination voting channel."""
- review, reviewed_emoji, nominee = await self.make_review(nomination)
+ review, reviewed_emoji, nominee, nominations = await self.make_review(nomination)
if not nominee:
return
@@ -238,16 +236,24 @@ class Reviewer:
channel = guild.get_channel(Channels.nomination_voting)
log.info(f"Posting the review of {nominee} ({nominee.id})")
- messages = await self._bulk_send(channel, review)
+ vote_message = await channel.send(review)
- last_message = messages[-1]
if reviewed_emoji:
for reaction in (reviewed_emoji, "\N{THUMBS UP SIGN}", "\N{THUMBS DOWN SIGN}"):
- await last_message.add_reaction(reaction)
+ await vote_message.add_reaction(reaction)
- thread = await last_message.create_thread(
+ thread = await vote_message.create_thread(
name=f"Nomination - {nominee}",
)
+
+ nomination_messages = []
+ for batch in nominations:
+ nomination_messages.append(await thread.send(batch))
+
+ # Pin the later messages first so the "Nominated by:" message is at the top of the pins list
+ for nom_message in nomination_messages[::-1]:
+ await nom_message.pin()
+
message = await thread.send(f"<@&{Roles.mod_team}> <@&{Roles.admins}>")
now = datetime.now(tz=UTC)
@@ -275,13 +281,9 @@ class Reviewer:
opening = f"{nominee.mention} ({nominee}) for Helper!"
- current_nominations = "\n\n".join(
- f"**<@{entry.actor_id}>:** {entry.reason or '*no reason given*'}"
- for entry in nomination.entries[::-1]
- )
- current_nominations = f"**Nominated by:**\n{current_nominations}"
+ nominations = self._make_nomination_batches(nomination.entries)
- review_body = await self._construct_review_body(nominee)
+ review_body = await self._construct_review_body(nominee, nomination)
reviewed_emoji = self._random_ducky(guild)
vote_request = (
@@ -290,8 +292,28 @@ class Reviewer:
" and react :+1: for approval, or :-1: for disapproval*."
)
- review = "\n\n".join((opening, current_nominations, review_body, vote_request))
- return review, reviewed_emoji, nominee
+ review = "\n\n".join((opening, review_body, vote_request))
+ return review, reviewed_emoji, nominee, nominations
+
+ def _make_nomination_batches(self, entries: list[NominationEntry]) -> list[str]:
+ """Construct the batches of nominations to send into the voting thread."""
+ messages = ["**Nominated by:**"]
+
+ formatted = [f"**<@{entry.actor_id}>:** {entry.reason or '*no reason given*'}" for entry in entries[::-1]]
+
+ for entry in formatted:
+ # Add the nomination to the current last message in the message batches
+ potential_message = messages[-1] + f"\n\n{entry}"
+
+ # Test if adding this entry pushes us over the character limit
+ if len(potential_message) >= MAX_MESSAGE_SIZE:
+ # If it does, create a new message starting with this entry
+ messages.append(entry)
+ else:
+ # If it doesn't, we will use this message
+ messages[-1] = potential_message
+
+ return messages
async def archive_vote(self, message: PartialMessage, passed: bool) -> None:
"""Archive this vote to #nomination-archive."""
@@ -305,45 +327,28 @@ class Reviewer:
except NotFound:
log.warning(f"Could not find a thread linked to {message.channel.id}-{message.id}")
- # We consider the first message in the nomination to contain the user ping, username#discrim, and fixed text
- messages = [message]
- if not NOMINATION_MESSAGE_REGEX.search(message.content):
- async for new_message in message.channel.history(before=message.created_at):
- messages.append(new_message)
-
- if NOMINATION_MESSAGE_REGEX.search(new_message.content):
- break
-
- log.debug(f"Found {len(messages)} messages: {', '.join(str(m.id) for m in messages)}")
-
- parts = []
- for message_ in messages[::-1]:
- parts.append(message_.content)
- parts.append("\n" if message_.content.endswith(".") else " ")
- content = "".join(parts)
-
# We assume that the first user mentioned is the user that we are voting on
- user_id = int(NOMINATION_MESSAGE_REGEX.search(content).group(1))
+ user_id = int(NOMINATION_MESSAGE_REGEX.search(message.content).group(1))
# Get reaction counts
reviewed = await count_unique_users_reaction(
- messages[0],
+ message,
lambda r: "ducky" in str(r) or str(r) == "\N{EYES}",
count_bots=False
)
upvotes = await count_unique_users_reaction(
- messages[0],
+ message,
lambda r: str(r) == "\N{THUMBS UP SIGN}",
count_bots=False
)
downvotes = await count_unique_users_reaction(
- messages[0],
+ message,
lambda r: str(r) == "\N{THUMBS DOWN SIGN}",
count_bots=False
)
# Remove the first and last paragraphs
- stripped_content = content.split("\n\n", maxsplit=1)[1].rsplit("\n\n", maxsplit=1)[0]
+ stripped_content = message.content.split("\n\n", maxsplit=1)[1].rsplit("\n\n", maxsplit=1)[0]
result = f"**Passed** {Emojis.incident_actioned}" if passed else f"**Rejected** {Emojis.incident_unactioned}"
colour = Colours.soft_green if passed else Colours.soft_red
@@ -367,34 +372,36 @@ class Reviewer:
embed_title = f"Vote for `{user_id}`"
channel = self.bot.get_channel(Channels.nomination_voting_archive)
- for number, part in enumerate(
- textwrap.wrap(embed_content, width=MAX_EMBED_SIZE, replace_whitespace=False, placeholder="")
- ):
- await channel.send(embed=Embed(
- title=embed_title if number == 0 else None,
- description="[...] " + part if number != 0 else part,
- colour=colour
- ))
-
- for message_ in messages:
- with contextlib.suppress(NotFound):
- await message_.delete()
+ await channel.send(embed=Embed(
+ title=embed_title,
+ description=embed_content,
+ colour=colour
+ ))
+
+ await message.delete()
if nomination_thread:
with contextlib.suppress(NotFound):
await nomination_thread.edit(archived=True)
- async def _construct_review_body(self, member: Member) -> str:
+ async def _construct_review_body(self, member: Member, nomination: Nomination) -> str:
"""Formats the body of the nomination, with details of activity, infractions, and previous nominations."""
activity = await self._activity_review(member)
+ nominations = await self._nominations_review(nomination)
infractions = await self._infractions_review(member)
prev_nominations = await self._previous_nominations_review(member)
- body = f"{activity}\n\n{infractions}"
+ body = f"{nominations}\n\n{activity}\n\n{infractions}"
if prev_nominations:
body += f"\n\n{prev_nominations}"
return body
+ async def _nominations_review(self, nomination: Nomination) -> str:
+ """Format a brief summary of how many nominations in this voting round the nominee has."""
+ entry_count = len(nomination.entries)
+
+ return f"They have **{entry_count}** nomination{'s' if entry_count != 1 else ''} this round."
+
async def _activity_review(self, member: Member) -> str:
"""
Format the activity of the nominee.
@@ -548,20 +555,3 @@ class Reviewer:
if not duckies:
return "\N{EYES}"
return random.choice(duckies)
-
- @staticmethod
- async def _bulk_send(channel: TextChannel, text: str) -> list[Message]:
- """
- Split a text into several if necessary, and post them to the channel.
-
- Returns the resulting message objects.
- """
- messages = textwrap.wrap(text, width=MAX_MESSAGE_SIZE, replace_whitespace=False)
- log.trace(f"The provided string will be sent to the channel {channel.id} as {len(messages)} messages.")
-
- results = []
- for message in messages:
- await asyncio.sleep(1)
- results.append(await channel.send(message))
-
- return results