Skip to content

Commit

Permalink
Generalize message validation system for showcase messages in 'showca…
Browse files Browse the repository at this point in the history
…se' extension, fix deploy-to-vps.yml
  • Loading branch information
Mega-JC committed Jul 26, 2024
1 parent a443a56 commit 43ecf3d
Show file tree
Hide file tree
Showing 8 changed files with 720 additions and 91 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/deploy-to-vps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ jobs:
SSH_PRIVATE_KEY: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.VPS_HOST }}
REMOTE_USER: ${{ secrets.VPS_USER }}
SOURCE: "."
TARGET: ~/PygameCommunityBot/
SCRIPT_AFTER: |
cp ~/config.py ~/PygameCommunityBot/config.py
cp ~/.env ~/PygameCommunityBot/.env
cd ~/PygameCommunityBot
docker compose stop
docker compose rm -f
sleep 60 && docker compose up -d --build
Expand Down
1 change: 0 additions & 1 deletion pcbot/exts/op.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ async def op_pin_func(
),
timeout=2,
)
print("HERE")
except asyncio.TimeoutError:
pass
else:
Expand Down
61 changes: 61 additions & 0 deletions pcbot/exts/showcase/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Collection
import discord
import snakecore

from .utils import ShowcaseChannelConfig

BotT = snakecore.commands.Bot | snakecore.commands.AutoShardedBot


@snakecore.commands.decorators.with_config_kwargs
async def setup(
bot: BotT,
showcase_channels_config: Collection[ShowcaseChannelConfig],
theme_color: int | discord.Color = 0,
):
# validate showcase channels config
for i, showcase_channel_config in enumerate(showcase_channels_config):
if "channel_id" not in showcase_channel_config:
raise ValueError("Showcase channel config must have a 'channel_id' key")
elif (
"default_auto_archive_duration" in showcase_channel_config
and not isinstance(
showcase_channel_config["default_auto_archive_duration"], int
)
):
raise ValueError(
"Showcase channel config 'default_auto_archive_duration' must be an integer"
)
elif (
"default_thread_slowmode_delay" in showcase_channel_config
and not isinstance(
showcase_channel_config["default_thread_slowmode_delay"], int
)
):
raise ValueError(
"Showcase channel config 'default_thread_slowmode_delay' must be an integer"
)
elif "showcase_message_rules" not in showcase_channel_config:
raise ValueError(
"Showcase channel config must have a 'showcase_message_rules' key"
)

from .utils import dispatch_rule_specifier_dict_validator, BadRuleSpecifier

specifier_dict_validator = dispatch_rule_specifier_dict_validator(
showcase_channel_config["showcase_message_rules"]
)

# validate 'showcase_message_rules' value
try:
specifier_dict_validator(
showcase_channel_config["showcase_message_rules"] # type: ignore
)
except BadRuleSpecifier as e:
raise ValueError(
f"Error while parsing config.{i}.showcase_message_rules field: {e}"
) from e

from .cogs import Showcasing

await bot.add_cog(Showcasing(bot, showcase_channels_config, theme_color))
145 changes: 57 additions & 88 deletions pcbot/exts/showcase.py → pcbot/exts/showcase/cogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
Copyright (c) 2022-present pygame-community.
"""

import abc
import asyncio
from collections.abc import Collection
import datetime
import enum
import itertools
import re
import time
from typing import NotRequired, TypedDict
from typing import Any, Callable, Literal, NotRequired, Protocol, TypedDict

import discord
from discord.ext import commands
Expand All @@ -18,17 +20,11 @@
from snakecore.commands import UnicodeEmoji
from snakecore.commands.converters import DateTime

from ..base import BaseExtensionCog

BotT = snakecore.commands.Bot | snakecore.commands.AutoShardedBot
from .utils import ShowcaseChannelConfig, validate_message

from ...base import BaseExtensionCog

class ShowcaseChannelConfig(TypedDict):
"""A typed dict for specifying showcase channel configurations."""

channel_id: int
default_auto_archive_duration: NotRequired[int]
default_thread_slowmode_delay: NotRequired[int]
BotT = snakecore.commands.Bot | snakecore.commands.AutoShardedBot


class Showcasing(BaseExtensionCog, name="showcasing"):
Expand Down Expand Up @@ -372,39 +368,21 @@ async def delete_bad_message_with_thread(
# don't error here if thread and/or message were already deleted
pass

@staticmethod
def showcase_message_validity_check(
message: discord.Message, min_chars=32, max_chars=float("inf")
):
"""Checks if a thread's starter message has the right format.
self,
message: discord.Message,
) -> tuple[bool, str | None]:
"""Checks if a showcase message has the right format.
Returns
-------
bool:
True/False
tuple[bool, str | None]:
A tuple containing a boolean indicating whether the message is valid or not, and a string describing the reason why it is invalid if it is not valid.
"""

search_obj = re.search(
snakecore.utils.regex_patterns.URL, message.content or ""
return validate_message(
message,
self.showcase_channels_config[message.channel.id]["showcase_message_rules"],
)
link_in_msg = bool(search_obj)
first_link_str = search_obj.group() if link_in_msg else ""

char_length = len(message.content) + len(
message.channel.name if isinstance(message.channel, discord.Thread) else ""
)

if (
message.content
and (link_in_msg and char_length > len(first_link_str))
and min_chars <= char_length < max_chars
):
return True

elif message.content and message.attachments:
return True

return False

@commands.Cog.listener()
async def on_thread_create(self, thread: discord.Thread):
Expand All @@ -419,15 +397,15 @@ async def on_thread_create(self, thread: discord.Thread):
except discord.NotFound:
return

if not self.showcase_message_validity_check(message):
is_valid, reason = self.showcase_message_validity_check(message)

if not is_valid:
deletion_datetime = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(minutes=5)
warn_msg = await message.reply(
"Your message must contain an attachment or text and safe links to be valid.\n\n"
"- Attachment-only entries must be in reference to a previous post of yours.\n"
"- Text-only posts must contain at least 32 characters (including their title "
"and including links, but not links alone).\n\n"
"### Invalid showcase message\n\n"
f"{reason}\n\n"
" If no changes are made, your message (and its thread/post) will be "
f"deleted {snakecore.utils.create_markdown_timestamp(deletion_datetime, 'R')}."
)
Expand Down Expand Up @@ -493,28 +471,31 @@ async def prompt_author_for_feedback_thread(self, message: discord.Message):
pass
else:
if snakecore.utils.is_emoji_equal(event.emoji, "✅"):
await message.create_thread(
name=(
f"Feedback for "
+ f"@{message.author.name} | {str(message.author.id)[-6:]}"
)[:100],
auto_archive_duration=(
self.showcase_channels_config[message.channel.id].get(
"default_auto_archive_duration", 60
)
if bot_perms.manage_threads
else discord.utils.MISSING
), # type: ignore
slowmode_delay=(
self.showcase_channels_config[message.channel.id].get(
"default_thread_slowmode_delay",
)
if bot_perms.manage_threads
else None
), # type: ignore
reason=f"A '#{message.channel.name}' message "
"author requested a feedback thread.",
)
try:
await message.create_thread(
name=(
f"Feedback for "
+ f"@{message.author.name} | {str(message.author.id)[-6:]}"
)[:100],
auto_archive_duration=(
self.showcase_channels_config[message.channel.id].get(
"default_auto_archive_duration", 60
)
if bot_perms.manage_threads
else discord.utils.MISSING
), # type: ignore
slowmode_delay=(
self.showcase_channels_config[message.channel.id].get(
"default_thread_slowmode_delay",
)
if bot_perms.manage_threads
else None
), # type: ignore
reason=f"A '#{message.channel.name}' message "
"author requested a feedback thread.",
)
except discord.HTTPException:
pass

try:
await alert_msg.delete()
Expand All @@ -533,17 +514,17 @@ async def on_message(self, message: discord.Message):
):
return

if self.showcase_message_validity_check(message):
is_valid, reason = self.showcase_message_validity_check(message)

if is_valid:
await self.prompt_author_for_feedback_thread(message)
else:
deletion_datetime = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(minutes=5)
warn_msg = await message.reply(
"Your message must contain an attachment or text and safe links to be valid.\n\n"
"- Attachment-only entries must be in reference to a previous post of yours.\n"
"- Text-only posts must contain at least 32 characters (including their title "
"and including links, but not links alone).\n\n"
"### Invalid showcase message\n\n"
f"{reason}\n\n"
" If no changes are made, your message (and its thread/post) will be "
f"deleted {snakecore.utils.create_markdown_timestamp(deletion_datetime, 'R')}."
)
Expand Down Expand Up @@ -575,7 +556,9 @@ async def on_message_edit(self, old: discord.Message, new: discord.Message):
):
return

if not self.showcase_message_validity_check(new):
is_valid, reason = self.showcase_message_validity_check(new)

if not is_valid:
if new.id in self.entry_message_deletion_dict:
deletion_data_tuple = self.entry_message_deletion_dict[new.id]
deletion_task = deletion_data_tuple[0]
Expand All @@ -594,14 +577,9 @@ async def on_message_edit(self, old: discord.Message, new: discord.Message):
) + datetime.timedelta(minutes=5)
await warn_msg.edit(
content=(
"I noticed your edit. However:\n\n"
"Your post must contain an attachment or text and safe "
"links to be valid.\n\n"
"- Attachment-only entries must be in reference to a "
"previous post of yours.\n"
"- Text-only posts must contain at least 32 "
"characters (including their title "
"and including links, but not links alone).\n\n"
"### Invalid showcase message\n\n"
"Your edited showcase message is invalid.\n\n"
f"{reason}\n\n"
" If no changes are made, your post will be "
f"deleted "
+ snakecore.utils.create_markdown_timestamp(
Expand Down Expand Up @@ -647,7 +625,7 @@ async def on_message_edit(self, old: discord.Message, new: discord.Message):
)

elif (
self.showcase_message_validity_check(new)
is_valid
) and new.id in self.entry_message_deletion_dict: # an invalid entry was corrected
deletion_data_tuple = self.entry_message_deletion_dict[new.id]
deletion_task = deletion_data_tuple[0]
Expand Down Expand Up @@ -787,12 +765,3 @@ async def on_raw_thread_delete(self, payload: discord.RawThreadDeleteEvent):
deletion_task.cancel()

del self.entry_message_deletion_dict[payload.thread_id]


@snakecore.commands.decorators.with_config_kwargs
async def setup(
bot: BotT,
showcase_channels_config: Collection[ShowcaseChannelConfig],
theme_color: int | discord.Color = 0,
):
await bot.add_cog(Showcasing(bot, showcase_channels_config, theme_color))
20 changes: 20 additions & 0 deletions pcbot/exts/showcase/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from abc import ABC
import re
from typing import Any, Callable, Collection, Literal, NotRequired, TypedDict
import discord
import snakecore

from .utils import *
from .validators import *

BotT = snakecore.commands.Bot | snakecore.commands.AutoShardedBot


class ShowcaseChannelConfig(TypedDict):
"""A typed dict for specifying showcase channel configurations."""

channel_id: int
default_auto_archive_duration: NotRequired[int]
default_thread_slowmode_delay: NotRequired[int]
showcase_message_rules: RuleSpecifier | RuleSpecifierPair | RuleSpecifierList
"A rule specifier dict for validating messages posted to the showcase channel"
Loading

0 comments on commit 43ecf3d

Please sign in to comment.