Skip to content

Commit

Permalink
Merge pull request #125 from ToxicBiohazard/main
Browse files Browse the repository at this point in the history
Spoilers are directed to the Jira
  • Loading branch information
dimoschi authored Nov 27, 2024
2 parents 9916b05 + f914e1d commit c8b9568
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 69 deletions.
4 changes: 3 additions & 1 deletion .test.env
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,7 @@ CURRENT_SEASON_ID=1
HTB_API_KEY=CHANGE_ME

#Feedback Webhook
SLACK_FEEDBACK_WEBHOOK="https://hook.slack.com/sdfsdfsf"

SLACK_WEBHOOK="https://hook.slack.com/sdfsdfsf"
#Feedback Webhook
JIRA_SPOILER_WEBHOOK="https://automation.atlassian.com/sdfsdfsf"
81 changes: 53 additions & 28 deletions src/cmds/core/other.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging

import aiohttp
import discord
from discord import ApplicationContext, Embed, Interaction, Message, Option, WebhookMessage, slash_command
from discord import ApplicationContext, Interaction, Message, Option, slash_command
from discord.ext import commands
from discord.ui import InputText, Modal
from slack_sdk.webhook import WebhookClient
Expand All @@ -16,16 +17,16 @@ class FeedbackModal(Modal):
"""Feedback modal."""

def __init__(self, *args, **kwargs) -> None:
"""Initialize the Feedback Modal with input fields."""
super().__init__(*args, **kwargs)

self.add_item(InputText(label="Title"))
self.add_item(InputText(label="Feedback", style=discord.InputTextStyle.long))

async def callback(self, interaction: discord.Interaction) -> None:
"""Callback for the feedback modal."""
"""Handle the modal submission by sending feedback to Slack."""
await interaction.response.send_message("Thank you, your feedback has been recorded.", ephemeral=True)

webhook = WebhookClient(settings.SLACK_WEBHOOK) # Establish Slack Webhook
webhook = WebhookClient(settings.SLACK_FEEDBACK_WEBHOOK)

if interaction.user: # Protects against some weird edge-cases
title = f"{self.children[0].value} - {interaction.user.name}"
Expand Down Expand Up @@ -53,21 +54,53 @@ async def callback(self, interaction: discord.Interaction) -> None:
assert response.body == "ok"


class SpoilerModal(Modal):
"""Modal for reporting a spoiler."""

def __init__(self, *args, **kwargs) -> None:
"""Initialize the Spoiler Modal with input fields."""
super().__init__(*args, **kwargs)
self.add_item(InputText(label="URL", placeholder="Enter the spoiler URL", style=discord.InputTextStyle.long))

async def callback(self, interaction: discord.Interaction) -> None:
"""Handle the modal submission by sending the spoiler report to JIRA."""
url = self.children[0].value.strip() # Trim any whitespace

if not url: # Check if the URL is empty
await interaction.response.send_message("Please provide the spoiler URL.", ephemeral=True)
return
await interaction.response.send_message("Thank you, the spoiler has been reported.", ephemeral=True)

user_name = interaction.user.display_name
url = self.children[0].value

webhook_url = settings.JIRA_SPOILER_WEBHOOK

payload = {
"user": user_name,
"url": url,
}

async with aiohttp.ClientSession() as session:
try:
async with session.post(webhook_url, json=payload) as response:
if response.status != 200:
logger.error(f"Failed to send to JIRA: {response.status} - {await response.text()}")
except Exception as e:
logger.error(f"Error sending to JIRA: {e}")


class OtherCog(commands.Cog):
"""Ban related commands."""
"""Other commands related to the bot."""

def __init__(self, bot: Bot):
self.bot = bot

@slash_command(guild_ids=settings.guild_ids, description="A simple reply stating hints are not allowed.")
async def no_hints(
self, ctx: ApplicationContext
) -> Message:
"""A simple reply stating hints are not allowed."""
async def no_hints(self, ctx: ApplicationContext) -> Message:
"""Reply stating that hints are not allowed."""
return await ctx.respond(
"No hints are allowed for the duration the event is going on. This is a competitive event with prizes. "
"Once the event is over you are more then welcome to share solutions/write-ups/etc and try them in the "
"After Party event."
"No hints are allowed for the duration of the event. Once the event is over, feel free to share solutions."
)

@slash_command(guild_ids=settings.guild_ids,
Expand All @@ -86,28 +119,20 @@ async def support(
"https://help.hackthebox.com/en/articles/5986762-contacting-htb-support"
)

@slash_command(guild_ids=settings.guild_ids, description="Add the URL which has spoiler link.")
async def spoiler(self, ctx: ApplicationContext, url: str) -> Interaction | WebhookMessage:
"""Add the URL which has spoiler link."""
if len(url) == 0:
return await ctx.respond("Please provide the spoiler URL.")

embed = Embed(title="Spoiler Report", color=0xB98700)
embed.add_field(name=f"{ctx.user} has submitted a spoiler.", value=f"URL: <{url}>", inline=False)

channel = self.bot.get_channel(settings.channels.SPOILER)
await channel.send(embed=embed)
return await ctx.respond("Thanks for the reporting the spoiler.", ephemeral=True, delete_after=15)
@slash_command(guild_ids=settings.guild_ids, description="Add a URL which contains a spoiler.")
async def spoiler(self, ctx: ApplicationContext) -> Interaction:
"""Report a URL that contains a spoiler."""
modal = SpoilerModal(title="Report Spoiler")
return await ctx.send_modal(modal)

@slash_command(guild_ids=settings.guild_ids, description="Provide feedback to HTB!")
@slash_command(guild_ids=settings.guild_ids, description="Provide feedback to HTB.")
@commands.cooldown(1, 60, commands.BucketType.user)
async def feedback(self, ctx: ApplicationContext) -> Interaction:
"""Provide Feedback to HTB."""
# Send the Modal defined above in Feedback Modal, which handles the callback
"""Provide feedback to HTB."""
modal = FeedbackModal(title="Feedback")
return await ctx.send_modal(modal)


def setup(bot: Bot) -> None:
"""Load the `ChannelManageCog` cog."""
"""Load the OtherCog cog."""
bot.add_cog(OtherCog(bot))
3 changes: 2 additions & 1 deletion src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ class Global(BaseSettings):
WEBHOOK_PORT: int = 1337
WEBHOOK_TOKEN: str = ""

SLACK_WEBHOOK: str = ""
SLACK_FEEDBACK_WEBHOOK: str = ""
JIRA_SPOILER_WEBHOOK: str = ""

ROOT: Path = None

Expand Down
82 changes: 43 additions & 39 deletions tests/src/cmds/core/test_other.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from unittest.mock import patch
from unittest.mock import AsyncMock, patch

import pytest
from discord import ApplicationContext

from src.bot import Bot
from src.cmds.core import other
from tests.helpers import MockTextChannel
from src.cmds.core.other import OtherCog, SpoilerModal
from src.core import settings


class TestOther:
Expand All @@ -12,7 +15,7 @@ class TestOther:
@pytest.mark.asyncio
async def test_no_hints(self, bot, ctx):
"""Test the response of the `no_hints` command."""
cog = other.OtherCog(bot)
cog = OtherCog(bot)
ctx.bot = bot

# Invoke the command.
Expand Down Expand Up @@ -80,45 +83,46 @@ async def test_support_urls_different(self, bot, ctx):
assert labs_url != academy_url

@pytest.mark.asyncio
async def test_spoiler_without_url(self, bot, ctx):
"""Test the response of the `spoiler` command without url."""
cog = other.OtherCog(bot)

# Invoke the command.
await cog.spoiler.callback(cog, ctx, url="")

args, kwargs = ctx.respond.call_args
content = args[0]

assert isinstance(content, str)

assert content == "Please provide the spoiler URL."
async def test_spoiler_modal_callback_with_url(self):
modal = SpoilerModal(title="Report Spoiler")
interaction = AsyncMock()
interaction.user.display_name = "TestUser"
modal.children[0].value = "http://example.com/spoiler"

with patch('aiohttp.ClientSession.post', new_callable=AsyncMock) as mock_post:
mock_post.return_value.__aenter__.return_value.status = 200
await modal.callback(interaction)
interaction.response.send_message.assert_called_once_with(
"Thank you, the spoiler has been reported.", ephemeral=True
)
await mock_post.assert_called_once()

@pytest.mark.asyncio
async def test_spoiler(self, bot, ctx):
"""Test the response of the `spoiler` command."""
cog = other.OtherCog(bot)
mock_channel = MockTextChannel()

with patch.object(bot, "get_channel", return_value=mock_channel):
# Invoke the command.
await cog.spoiler.callback(cog, ctx, "https://www.definitely-a-spoiler.com/")

args, kwargs = ctx.respond.call_args
content = args[0]
ephemeral = kwargs.get("ephemeral")
delete_after = kwargs.get("delete_after")

# Command should respond with an embed.
assert mock_channel.send.call_count == 1
assert isinstance(content, str)
assert isinstance(ephemeral, bool)
assert isinstance(delete_after, int)

assert content == "Thanks for the reporting the spoiler."
assert ephemeral is True
assert delete_after == 15
async def test_spoiler_modal_callback_with_url(self):
modal = SpoilerModal(title="Report Spoiler")
interaction = AsyncMock()
interaction.user.display_name = "TestUser"
modal.children[0].value = "http://example.com/spoiler"

with patch('aiohttp.ClientSession.post', new_callable=AsyncMock) as mock_post:
mock_post.return_value.__aenter__.return_value.status = 200
await modal.callback(interaction)
interaction.response.send_message.assert_called_once_with(
"Thank you, the spoiler has been reported.", ephemeral=True
)
mock_post.assert_called_once()

@pytest.mark.asyncio
async def test_spoiler_modal_callback_without_url(self):
modal = SpoilerModal(title="Report Spoiler")
interaction = AsyncMock()
interaction.user.display_name = "TestUser"
modal.children[0].value = ""

await modal.callback(interaction)
interaction.response.send_message.assert_called_once_with(
"Please provide the spoiler URL.", ephemeral=True
)
def test_setup(self, bot):
"""Test the setup method of the cog."""
# Invoke the command
Expand Down

0 comments on commit c8b9568

Please sign in to comment.