Skip to content

Commit

Permalink
Merge pull request #115 from janssensjelle/feature/add-evidence-to-kick
Browse files Browse the repository at this point in the history
[FEATURE] Evidence field for `/kick`
  • Loading branch information
dimoschi authored Nov 21, 2024
2 parents 2fa9e6b + 24b2b86 commit 9e7d1b0
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 27 deletions.
21 changes: 4 additions & 17 deletions src/cmds/core/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

from src.bot import Bot
from src.core import settings
from src.database.models import Ban, Infraction, UserNote
from src.database.models import Ban, Infraction
from src.database.session import AsyncSessionLocal
from src.helpers.ban import add_infraction, ban_member, unban_member
from src.helpers.ban import add_evidence_note, add_infraction, ban_member, unban_member
from src.helpers.duration import validate_duration
from src.helpers.schedule import schedule

Expand All @@ -34,7 +34,7 @@ async def ban(
member = await self.bot.get_member_or_user(ctx.guild, user.id)
if not member:
return await ctx.respond(f"User {user} not found.")
await self.add_evidence_note(member.id, reason, evidence, ctx.user.id)
await add_evidence_note(member.id, "ban", reason, evidence, ctx.user.id)
response = await ban_member(self.bot, ctx.guild, member, "500w", reason, ctx.user, needs_approval=False)
return await ctx.respond(response.message, delete_after=response.delete_after)

Expand All @@ -52,7 +52,7 @@ async def tempban(
member = await self.bot.get_member_or_user(ctx.guild, user.id)
if not member:
return await ctx.respond(f"User {user} not found.")
await self.add_evidence_note(member.id, reason, evidence, ctx.user.id)
await add_evidence_note(member.id, "ban", reason, evidence, ctx.user.id)
response = await ban_member(self.bot, ctx.guild, member, duration, reason, ctx.user, needs_approval=True)
return await ctx.respond(response.message, delete_after=response.delete_after)

Expand Down Expand Up @@ -190,19 +190,6 @@ async def remove_infraction(self, ctx: ApplicationContext, infraction_id: int) -
else:
return await ctx.respond(f"Infraction record #{infraction_id} has not been found.")

async def add_evidence_note(
self, user_id: int, reason: str, evidence: str, moderator_id: int
) -> None:
"""Add a note with evidence to the user's history records."""
if not evidence:
evidence = "none provided"
note = f"Reason for ban: {reason} (Evidence: {evidence})"
today = arrow.utcnow().format("YYYY-MM-DD")
user_note = UserNote(user_id=user_id, note=note, date=today, moderator_id=moderator_id)
async with AsyncSessionLocal() as session:
session.add(user_note)
await session.commit()


def setup(bot: Bot) -> None:
"""Load the `BanCog` cog."""
Expand Down
9 changes: 6 additions & 3 deletions src/cmds/core/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logging
import os
import random
from typing import Tuple, Union
from datetime import datetime
from typing import Tuple, Union

import discord
from discord import Interaction, Member, Option, User, WebhookMessage
Expand All @@ -16,8 +16,8 @@
from src.core import settings
from src.database.models import HtbDiscordLink
from src.database.session import AsyncSessionLocal
from src.helpers.ban import add_evidence_note, add_infraction
from src.helpers.checks import member_is_staff
from src.helpers.ban import add_infraction

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -63,7 +63,8 @@ async def bad_name(self, ctx: ApplicationContext, user: Member) -> Interaction |

@slash_command(guild_ids=settings.guild_ids, description="Kick a user from the server.")
@has_any_role(*settings.role_groups.get("ALL_ADMINS"), *settings.role_groups.get("ALL_MODS"))
async def kick(self, ctx: ApplicationContext, user: Member, reason: str) -> Interaction | WebhookMessage:
async def kick(self, ctx: ApplicationContext, user: Member, reason: str, evidence: str = None) \
-> Interaction | WebhookMessage:
"""Kick a user from the server."""
member = await self.bot.get_member_or_user(ctx.guild, user.id)
if not member:
Expand All @@ -78,6 +79,8 @@ async def kick(self, ctx: ApplicationContext, user: Member, reason: str) -> Inte
if len(reason) == 0:
reason = "No reason given..."

await add_evidence_note(member.id, "kick", reason, evidence, ctx.user.id)

try:
await member.send(f"You have been kicked from {ctx.guild.name} for the following reason:\n>>> {reason}\n")
except Forbidden as ex:
Expand Down
24 changes: 21 additions & 3 deletions src/helpers/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import logging
from datetime import datetime

import arrow
import discord
from discord import Forbidden, Guild, HTTPException, Member, NotFound, User
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound

from src.bot import Bot
from src.core import settings
from src.database.models import Ban, Infraction, Mute
from src.database.models import Ban, Infraction, Mute, UserNote
from src.database.session import AsyncSessionLocal
from src.helpers.checks import member_is_staff
from src.helpers.duration import validate_duration
Expand Down Expand Up @@ -63,7 +64,9 @@ async def ban_member(

# Validate duration
dur, dur_exc = validate_duration(duration)
# Check if duration is valid, negative values are generally not allowed, so they should be caught here
# Check if duration is valid,
# negative values are generally not allowed,
# so they should be caught here
if dur <= 0:
return SimpleResponse(message=dur_exc, delete_after=15)
else:
Expand Down Expand Up @@ -140,7 +143,8 @@ async def ban_member(
member_name = f"{member.display_name} ({member.name})"
embed = discord.Embed(
title=f"Ban request #{ban_id}",
description=f"{author.display_name} ({author.name}) would like to ban {member_name} until {end_date} (UTC). Reason: {reason}", )
description=f"{author.display_name} ({author.name}) "
f"would like to ban {member_name} until {end_date} (UTC). Reason: {reason}", )
embed.set_thumbnail(url=f"{settings.HTB_URL}/images/logo600.png")
view = BanDecisionView(ban_id, bot, guild, member, end_date, reason)
await guild.get_channel(settings.channels.SR_MOD).send(embed=embed, view=view)
Expand Down Expand Up @@ -281,3 +285,17 @@ async def add_infraction(
logger.warning(f"HTTPException when trying to add infraction for user with ID {member.id}", exc_info=ex)

return SimpleResponse(message=message, delete_after=None)


async def add_evidence_note(
user_id: int, action: str, reason: str, evidence: str, moderator_id: int
) -> None:
"""Add a note with evidence to the user's history records."""
if not evidence:
evidence = "none provided"
note = f"Reason for {action}: {reason} (Evidence: {evidence})"
today = arrow.utcnow().format("YYYY-MM-DD")
user_note = UserNote(user_id=user_id, note=note, date=today, moderator_id=moderator_id)
async with AsyncSessionLocal() as session:
session.add(user_note)
await session.commit()
8 changes: 4 additions & 4 deletions tests/src/cmds/core/test_ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async def test_ban_success(self, ctx, bot):
bot.get_member_or_user.return_value = user

with patch('src.cmds.core.ban.ban_member', new_callable=AsyncMock) as ban_member_mock, \
patch('src.cmds.core.ban.BanCog.add_evidence_note', new_callable=AsyncMock) as add_evidence_note_mock:
patch('src.cmds.core.ban.add_evidence_note', new_callable=AsyncMock) as add_evidence_note_mock:
ban_response = SimpleResponse(
message=f"Member {user.display_name} has been banned permanently.", delete_after=0
)
Expand All @@ -32,7 +32,7 @@ async def test_ban_success(self, ctx, bot):
await cog.ban.callback(cog, ctx, user, "Any valid reason", "Some evidence")

# Assertions
add_evidence_note_mock.assert_called_once_with(user.id, "Any valid reason", "Some evidence", ctx.user.id)
add_evidence_note_mock.assert_called_once_with(user.id, "ban", "Any valid reason", "Some evidence", ctx.user.id)
ban_member_mock.assert_called_once_with(
bot, ctx.guild, user, "500w", "Any valid reason", ctx.user, needs_approval=False
)
Expand All @@ -48,7 +48,7 @@ async def test_tempban_success(self, ctx, bot):

with patch('src.helpers.ban.validate_duration', new_callable=AsyncMock) as validate_duration_mock, \
patch('src.cmds.core.ban.ban_member', new_callable=AsyncMock) as ban_member_mock, \
patch('src.cmds.core.ban.BanCog.add_evidence_note', new_callable=AsyncMock) as add_evidence_note_mock:
patch('src.cmds.core.ban.add_evidence_note', new_callable=AsyncMock) as add_evidence_note_mock:
validate_duration_mock.return_value = (calendar.timegm(time.gmtime()) + parse_duration_str("5d"), "")
ban_response = SimpleResponse(
message=f"Member {user.display_name} has been banned temporarily.", delete_after=0
Expand All @@ -59,7 +59,7 @@ async def test_tempban_success(self, ctx, bot):
await cog.tempban.callback(cog, ctx, user, "5d", "Any valid reason", "Some evidence")

# Assertions
add_evidence_note_mock.assert_called_once_with(user.id, "Any valid reason", "Some evidence", ctx.user.id)
add_evidence_note_mock.assert_called_once_with(user.id, "ban", "Any valid reason", "Some evidence", ctx.user.id)
ban_member_mock.assert_called_once_with(
bot, ctx.guild, user, "5d", "Any valid reason", ctx.user, needs_approval=True
)
Expand Down
30 changes: 30 additions & 0 deletions tests/src/cmds/core/test_user.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
from unittest.mock import AsyncMock, patch

import pytest

from src.cmds.core import user
from tests import helpers


class TestUserCog:
"""Test the `User` cog."""

@pytest.mark.asyncio
async def test_kick_success(self, ctx, bot):
ctx.user = helpers.MockMember(id=1, name="Test Moderator")
user_to_kick = helpers.MockMember(id=2, name="User to Kick", bot=False)
ctx.guild.kick = AsyncMock()
bot.get_member_or_user = AsyncMock(return_value=user_to_kick)

# Mock the DM channel
user_to_kick.send = AsyncMock()
user_to_kick.name = "User to Kick"

with (
patch('src.cmds.core.user.add_evidence_note', new_callable=AsyncMock) as add_evidence_mock,
patch('src.cmds.core.user.member_is_staff', return_value=False
):
cog = user.UserCog(bot)
await cog.kick.callback(cog, ctx, user_to_kick, "Violation of rules")

add_evidence_mock.assert_called_once_with(user_to_kick.id, "kick", "Violation of rules", None, ctx.user.id)

# Assertions
ctx.guild.kick.assert_called_once_with(user=user_to_kick, reason="Violation of rules")
ctx.respond.assert_called_once_with("User to Kick got the boot!")


def test_setup(self, bot):
"""Test the setup method of the cog."""
# Invoke the command
Expand Down

0 comments on commit 9e7d1b0

Please sign in to comment.