Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ api_ninja_key=
currency_api_key=

# 14 character salt
salt=
salt=

log_channel=
dev_help_forum_id=
3 changes: 1 addition & 2 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ database:
password: !ENV "db_pass"
port: !ENV "db_port"


api:
api_ninja: !ENV "api_ninja_key"

guild:
staff_roles: [ 748554755265134635 ]
staff_roles: [748554755265134635]
dev_help_forum: 1020260384638259210
log_channel: 1084008856633491557

Expand Down
12 changes: 6 additions & 6 deletions config/dev-config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
bot:
prefix: "pph-"
token: !ENV "token"
staff_roles: [ 748554755265134635, 809311700560904201, 1064179977639165952 ]
staff_roles: [1062998739012296795]

database:
name: !ENV "db_name"
Expand All @@ -14,10 +14,10 @@ api:
api_ninja: !ENV "api_ninja_key"

guild:
staff_roles: [ 748554755265134635, 809311700560904201, 1064179977639165952 ]
dev_help_forum: 1080527620434841731
log_channel: 1084008856633491557
staff_roles: [1062998739012296795] # [748554755265134635]
dev_help_forum: !ENV "dev_help_forum_id"
log_channel: !ENV "log_channel"

logger:
log_channel: 1084008856633491557
log_level: "DEBUG"
log_channel: !ENV "log_channel"
log_level: "DEBUG"
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
progphilbot-db:
image: postgres:18
container_name: progphilbot-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: progphilbot-db
ports:
- "5434:5432"
volumes:
- progphilbot-volume:/var/lib/postgresql
networks:
- progphilbot-network

volumes:
progphilbot-volume:

networks:
progphilbot-network:
29 changes: 3 additions & 26 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ flake8 = "^7.1.0"
asyncpg = "^0.29.0"
requests = "^2.28.2"
yoyo-migrations = "^8.2.0"
psycopg2 = "^2.9.5"
cloudscraper = "^1.2.69"
bs4 = "^0.0.2"
psycopg2-binary = "^2.9.5"
Expand Down
4 changes: 0 additions & 4 deletions pyrightconfig.json

This file was deleted.

55 changes: 55 additions & 0 deletions src/cogs/admin/edit_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from discord import Interaction, NotFound
from discord.app_commands import command
from discord.app_commands.commands import describe
from discord.ext.commands.bot import Bot
from discord.ext.commands.cog import Cog

from data.admin.config_auto import Config
from ui.modals.edit_post import EditPostModal
from utils.decorators import is_staff


class EditPost(Cog):
def __init__(self, bot: Bot):
self.bot = bot
self.config = Config(self.bot.pool)

@is_staff()
@command(
name="post-edit",
description="A command to allow staff to edit an existing post created by the PPH bot",
)
@describe(message_id="The ID of the post/message to edit")
async def edit_post(self, interaction: Interaction, message_id: str):

if not interaction.channel_id:
return

try:
message = await interaction.channel.fetch_message(int(message_id))
except (ValueError, NotFound):
await interaction.response.send_message(
"Message not found in this channel.", ephemeral=True
)
return

if self.bot.user is not None and message.author.id != self.bot.user.id:
await interaction.response.send_message(
"Cannot edit message that isn't from PPH bot!", ephemeral=True
)
return

post_id = message.id
content = message.content
edit_post_modal = EditPostModal(
channel_id=interaction.channel_id,
post_id=post_id,
original_content=content,
bot=self.bot,
)

await interaction.response.send_modal(edit_post_modal)


async def setup(bot: Bot):
await bot.add_cog(EditPost(bot))
188 changes: 188 additions & 0 deletions src/cogs/admin/test_edit_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import unittest
from unittest.async_case import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, MagicMock

from discord import Interaction, NotFound

from ui.modals.edit_post import EditPostModal

from src.cogs.admin.edit_post import EditPost


class TestEditPost(IsolatedAsyncioTestCase):
def setUp(self):
self.mock_bot = MagicMock()
self.mock_bot.pool = MagicMock()
self.mock_bot.user = MagicMock()
self.mock_bot.user.id = 123456789
self.cog = EditPost(self.mock_bot)

def _make_interaction(self, *, channel_id=999, done=False):
interaction = AsyncMock(spec=Interaction)
interaction.response = AsyncMock()
interaction.response.send_modal = AsyncMock()
interaction.response.send_message = AsyncMock()
interaction.response.is_done = MagicMock(return_value=done)
interaction.channel = AsyncMock()
interaction.channel_id = channel_id
interaction.user = MagicMock()
return interaction

async def test_edit_post_sends_modal_for_valid_bot_message(self):
interaction = self._make_interaction()

mock_message = AsyncMock()
mock_message.id = 42
mock_message.content = "original content"
mock_message.author = MagicMock()
mock_message.author.id = self.mock_bot.user.id

interaction.channel.fetch_message = AsyncMock(return_value=mock_message)

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="42",
)

interaction.response.send_modal.assert_awaited_once()
sent_modal = interaction.response.send_modal.call_args[0][0]
self.assertIsInstance(sent_modal, EditPostModal)
self.assertEqual(sent_modal.post_id, 42)
self.assertEqual(sent_modal.channel_id, 999)
self.assertEqual(sent_modal.message.default, "original content")

async def test_edit_post_message_not_found(self):
interaction = self._make_interaction()
interaction.channel.fetch_message = AsyncMock(
side_effect=NotFound(MagicMock(), "not found")
)

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="42",
)

interaction.response.send_message.assert_awaited_once_with(
"Message not found in this channel.", ephemeral=True
)
interaction.response.send_modal.assert_not_awaited()

async def test_edit_post_invalid_message_id(self):
interaction = self._make_interaction()

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="not-an-int",
)

interaction.response.send_message.assert_awaited_once_with(
"Message not found in this channel.", ephemeral=True
)
interaction.response.send_modal.assert_not_awaited()

async def test_edit_post_refuses_non_bot_message(self):
interaction = self._make_interaction()

mock_message = AsyncMock()
mock_message.id = 42
mock_message.content = "someone else's post"
mock_message.author = MagicMock()
mock_message.author.id = 999999999 # not the bot

interaction.channel.fetch_message = AsyncMock(return_value=mock_message)

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="42",
)

interaction.response.send_message.assert_awaited_once_with(
"Cannot edit message that isn't from PPH bot!", ephemeral=True
)
interaction.response.send_modal.assert_not_awaited()

async def test_edit_post_returns_early_when_no_channel_id(self):
interaction = self._make_interaction(channel_id=None)

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="42",
)

interaction.response.send_message.assert_not_awaited()
interaction.response.send_modal.assert_not_awaited()

async def test_edit_post_refuses_non_bot_message_when_bot_user_is_none(self):
self.mock_bot.user = None
interaction = self._make_interaction()

mock_message = AsyncMock()
mock_message.id = 42
mock_message.content = "some content"
mock_message.author = MagicMock()
mock_message.author.id = 999999999

interaction.channel.fetch_message = AsyncMock(return_value=mock_message)

await self.cog.edit_post.callback(
self.cog,
interaction=interaction,
message_id="42",
)

# bot.user is None, so the author check is skipped, modal should open
interaction.response.send_modal.assert_awaited_once()


class TestEditPostModal(IsolatedAsyncioTestCase):
async def test_modal_default_content_is_set(self):
modal = EditPostModal(
channel_id=999,
post_id=42,
original_content="edit me",
bot=MagicMock(),
)

self.assertEqual(modal.message.default, "edit me")
self.assertEqual(modal.post_id, 42)
self.assertEqual(modal.channel_id, 999)

async def test_on_submit_fetches_and_edits_message(self):
mock_bot = MagicMock()
mock_channel = AsyncMock()
mock_message = AsyncMock()

mock_bot.fetch_channel = AsyncMock(return_value=mock_channel)
mock_channel.fetch_message = AsyncMock(return_value=mock_message)
mock_message.edit = AsyncMock()

mock_interaction = AsyncMock(spec=Interaction)
mock_interaction.response = AsyncMock()
mock_interaction.response.send_message = AsyncMock()

modal = EditPostModal(
channel_id=999,
post_id=42,
original_content="old content",
bot=mock_bot,
)
modal.message = MagicMock()
modal.message.value = "new content"

await modal.on_submit(mock_interaction)

mock_bot.fetch_channel.assert_awaited_once_with(999)
mock_channel.fetch_message.assert_awaited_once_with(42)
mock_message.edit.assert_awaited_once_with(content="new content")
mock_interaction.response.send_message.assert_awaited_once_with(
"Success", ephemeral=True
)


if __name__ == "__main__":
unittest.main()
Loading
Loading