Skip to content
127 changes: 127 additions & 0 deletions cogs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from asyncpg import Record, Connection
from cogs.reminder import Timer
from cogs.dpy import DPYExclusive
from cogs.reminder import Reminder as ReminderCog, Timer


DISCORD_API_ID = 81384788765712384
Expand All @@ -33,6 +34,7 @@
DISCORD_PY_JP_STAFF_ROLE = 490320652230852629
DISCORD_PY_PROF_ROLE = 381978395270971407
DISCORD_PY_HELPER_ROLE = 558559632637952010
DISCORD_PY_NO_GENERAL_ROLE = 1258249274899169290
# DISCORD_PY_HELP_CHANNELS = (381965515721146390, 738572311107469354, 985299059441025044)
DISCORD_PY_HELP_CHANNEL = 985299059441025044

Expand All @@ -56,6 +58,10 @@ def is_discord_py_helper(member: discord.Member) -> bool:

return member._roles.has(DISCORD_PY_HELPER_ROLE)

def can_use_no_general(member: discord.Member) -> bool:
# Using `ban_members` over `manage_roles` since Documentation Manager has that
return member.guild_permissions.ban_members or is_discord_py_helper(member)


def can_use_block():
def predicate(ctx: GuildContext) -> bool:
Expand Down Expand Up @@ -168,6 +174,21 @@ async def convert(self, ctx: GuildContext, argument: str):
return user


class CreateHelpThreadModal(discord.ui.Modal, title='Create help thread'):
thread_name = discord.ui.TextInput(label='Thread title', placeholder='Name for the help thread...', min_length=20, max_length=100)
should_mute = discord.ui.TextInput(label='Apply mute?', default="Yes", min_length=2, max_length=3)

def __init__(self) -> None:
super().__init__(custom_id='dpy-create-thread-modal')

async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
await interaction.response.send_message(f'Sorry, something went wrong.\n\n{error}', ephemeral=True)

async def on_submit(self, interaction: discord.Interaction) -> None:
self.stop()
await interaction.response.send_message('Thread created now.', ephemeral=True)


class RepositoryExample(NamedTuple):
path: str
url: str
Expand All @@ -186,6 +207,8 @@ class API(commands.Cog):
def __init__(self, bot: RoboDanny):
self.bot: RoboDanny = bot
self.issue = re.compile(r'##(?P<number>[0-9]+)')
self.create_thread_context = discord.app_commands.ContextMenu(name='Create help thread', callback=self.create_thread_callback)
self.bot.tree.add_command(self.create_thread_context, guild=discord.Object(id=DISCORD_PY_GUILD))

@property
def display_emoji(self) -> discord.PartialEmoji:
Expand All @@ -200,6 +223,84 @@ async def on_member_join(self, member: discord.Member):
role = discord.Object(id=USER_BOTS_ROLE)
await member.add_roles(role)

def cog_unload(self) -> None:
self.bot.tree.remove_command(
self.create_thread_context.name,
guild=discord.Object(id=DISCORD_PY_GUILD),
type=self.create_thread_context.type
)

async def _attempt_general_block(self, moderator: discord.Member, member: Union[discord.User, discord.Member]) -> None:
reminder: Optional[ReminderCog] = self.bot.get_cog('Reminder') # type: ignore # type downcasting
if not reminder:
return # we can't apply the timed role.

resolved = moderator.guild.get_member(member.id) if isinstance(member, discord.User) else member

if not resolved:
return # left the guild(?)

now = discord.utils.utcnow()
then = now + datetime.timedelta(hours=1)

await resolved.add_roles(discord.Object(id=DISCORD_PY_NO_GENERAL_ROLE), reason=f'Temp no general role by {moderator} (ID: {moderator.id}) until {discord.utils.format_dt(then), "F"}')

await reminder.create_timer(
then,
'general_block',
moderator.id,
member.id,
created=now
)

@commands.Cog.listener()
async def on_general_block_timer_complete(self, timer: Timer) -> None:
moderator_id, member_id = timer.args
await self.bot.wait_until_ready()

guild = self.bot.get_guild(DISCORD_PY_GUILD)
if guild is None:
# RIP
return

member = await self.bot.get_or_fetch_member(guild, member_id)
if member is None:
# They left the guild
return

moderator = await self.bot.get_or_fetch_member(guild, moderator_id)
if moderator is None:
try:
moderator = await self.bot.fetch_user(moderator_id)
except discord.HTTPException:
moderator = f'Mod ID: {moderator_id}'
else:
moderator = f'{moderator} (ID: {moderator_id})'

reason = f'Automatic removal of role from timer made on {timer.created_at} by {moderator}.'
await member.remove_roles(discord.Object(id=DISCORD_PY_NO_GENERAL_ROLE), reason=reason)


async def create_thread_callback(self, interaction: discord.Interaction, message: discord.Message) -> None:
if not can_use_no_general(interaction.user): # type: ignore # discord.Member since we're guild guarded
return await interaction.response.send_message('Sorry, this command is not available to you!')

modal = CreateHelpThreadModal()
await interaction.response.send_modal(modal)
if await modal.wait():
return # we return on timeout, rather than proceeding

forum: discord.ForumChannel = interaction.guild.get_channel(DISCORD_PY_HELP_CHANNEL) # pyright: ignore[reportAssignmentType,reportOptionalMemberAccess] # we know the type via ID and that guild is present
thread, _ = await forum.create_thread(
name=modal.thread_name.value,
content=message.content,
files=[await attachment.to_file() for attachment in message.attachments]
)
await thread.send(f'This thread was created on behalf of {message.author.mention}. Please continue your discussion for help in here.')

if modal.should_mute.value.lower() == "yes":
await self._attempt_general_block(interaction.user, message.author) # pyright: ignore[reportArgumentType] # can only be executed from the guild

def parse_object_inv(self, stream: SphinxObjectFileReader, url: str) -> dict[str, str]:
# key: URL
# n.b.: key doesn't have `discord` or `discord.ext.commands` namespaces
Expand Down Expand Up @@ -595,6 +696,32 @@ async def tempblock(self, ctx: GuildContext, duration: time.FutureTime, *, membe
else:
await ctx.send(f'Blocked {member} for {time.format_relative(duration.dt)}.')

@commands.command(name="tempnogeneral", aliases=["tempng"])
@can_use_tempblock()
async def temp_no_general(self, ctx: GuildContext, duration: time.FutureTime, *, member: discord.Member):
"""Temporarily blocks a user from your the general category of channels.

The duration can be a a short time form, e.g. 30d or a more human
duration such as "until thursday at 3PM" or a more concrete time
such as "2017-12-31".

Note that times are in UTC.
"""

if member.top_role >= ctx.author.top_role:
return

created_at = ctx.message.created_at
if is_discord_py_helper(ctx.author) and duration.dt > (created_at + datetime.timedelta(minutes=60)):
return await ctx.send('Helpers can only block for up to an hour.')

try:
await self._attempt_general_block(ctx.author, member)
except:
await ctx.send('\N{THUMBS DOWN SIGN}')
else:
await ctx.send(f'Blocked {member} for {time.format_relative(duration.dt)}.')

@commands.Cog.listener()
async def on_tempblock_timer_complete(self, timer: Timer):
guild_id, mod_id, channel_id, member_id = timer.args
Expand Down