aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-20 22:10:38 -0700
committerFuwn <[email protected]>2025-09-20 22:10:38 -0700
commite7ae17f31cc05279fa300c340587fc807af7b872 (patch)
tree4975ccaa7999870a750d141d26195b63ba1b1626
parentfeat(irm): Update post removal message subject (diff)
downloadumabot-e7ae17f31cc05279fa300c340587fc807af7b872.tar.xz
umabot-e7ae17f31cc05279fa300c340587fc807af7b872.zip
feat(irm): Discord logging
-rw-r--r--DISCORD_TEST_README.md57
-rw-r--r--env.example6
-rw-r--r--requirements.txt1
-rw-r--r--src/umabot/config.py6
-rw-r--r--src/umabot/discord_client.py165
-rw-r--r--src/umabot/rules/intelligent_roleplay_moderator.py39
-rwxr-xr-xtest_discord_embed.py154
7 files changed, 428 insertions, 0 deletions
diff --git a/DISCORD_TEST_README.md b/DISCORD_TEST_README.md
new file mode 100644
index 0000000..e38fb70
--- /dev/null
+++ b/DISCORD_TEST_README.md
@@ -0,0 +1,57 @@
+# Discord Embed Test Script
+
+This script allows you to test the Discord embed functionality without running the full bot.
+
+## Setup
+
+1. **Configure your `.env` file** with Discord webhook settings:
+
+ ```env
+ DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your_webhook_url_here
+ DISCORD_LOG_CHANNEL_ID=1419186821509939300
+ ```
+
+2. **Install dependencies** (if not already installed):
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+## Usage
+
+Run the test script:
+
+```bash
+python test_discord_embed.py
+```
+
+Or make it executable and run directly:
+
+```bash
+chmod +x test_discord_embed.py
+./test_discord_embed.py
+```
+
+## What it tests
+
+The script will send 4 test embeds to your Discord channel:
+
+1. **๐ŸŸข Reflair Action**: Tests the green embed for reflair actions
+2. **๐Ÿ”ด Removal Action**: Tests the red embed for removal actions
+3. **๐Ÿ”ด Long Reason Truncation**: Tests how long reasons are handled (truncated to 100 chars + "...")
+4. **๐Ÿ”ต Custom Embed**: Tests a custom blue embed with various fields
+
+## Expected Output
+
+If successful, you should see:
+
+- โœ… Success messages in the terminal
+- 4 embed messages in your Discord channel
+- Color-coded embeds (green, red, blue)
+- Proper formatting with emojis and fields
+- Long reason truncation demonstration
+
+## Troubleshooting
+
+- **"DISCORD_WEBHOOK_URL not found"**: Make sure your `.env` file has the webhook URL
+- **"Failed to send embed"**: Check that your webhook URL is valid and the bot has permission to send messages
+- **Import errors**: Make sure you're running from the project root directory
diff --git a/env.example b/env.example
index d01bc34..ed883fe 100644
--- a/env.example
+++ b/env.example
@@ -38,3 +38,9 @@ DRY_RUN=false
# Mod mail reasoning level (0=none, 1=brief, 2=full)
REASONING_LEVEL=2
+
+# Discord Webhook Configuration
+# Discord webhook URL for logging moderation actions
+DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
+# Discord channel ID for logging (optional, overrides webhook's default channel)
+DISCORD_LOG_CHANNEL_ID=1419186821509939300
diff --git a/requirements.txt b/requirements.txt
index 9eec680..3451d51 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ python-dotenv>=1.0.0
schedule>=1.2.0
loguru>=0.7.0
openai>=1.0.0
+requests>=2.31.0
diff --git a/src/umabot/config.py b/src/umabot/config.py
index 6d8ebdc..2d87ee1 100644
--- a/src/umabot/config.py
+++ b/src/umabot/config.py
@@ -39,6 +39,10 @@ class Config:
# Mod mail reasoning level (0=none, 1=brief, 2=full)
reasoning_level: int = 2
+ # Discord webhook configuration
+ discord_webhook_url: Optional[str] = None
+ discord_log_channel_id: Optional[str] = None
+
@classmethod
def from_env(cls) -> "Config":
"""Create configuration from environment variables."""
@@ -61,6 +65,8 @@ class Config:
roleplay_limit_window_hours=int(os.getenv("ROLEPLAY_LIMIT_WINDOW_HOURS", "24")),
dry_run=os.getenv("DRY_RUN", "false").lower() == "true",
reasoning_level=int(os.getenv("REASONING_LEVEL", "2")),
+ discord_webhook_url=os.getenv("DISCORD_WEBHOOK_URL"),
+ discord_log_channel_id=os.getenv("DISCORD_LOG_CHANNEL_ID"),
)
def validate(self) -> None:
diff --git a/src/umabot/discord_client.py b/src/umabot/discord_client.py
new file mode 100644
index 0000000..e700936
--- /dev/null
+++ b/src/umabot/discord_client.py
@@ -0,0 +1,165 @@
+"""Discord webhook client for logging moderation actions."""
+
+import requests
+import json
+from typing import Optional
+from loguru import logger
+
+
+class DiscordWebhookClient:
+ """Client for sending messages to Discord via webhooks."""
+
+ def __init__(self, webhook_url: str, channel_id: Optional[str] = None):
+ """Initialize the Discord webhook client.
+
+ Args:
+ webhook_url: The Discord webhook URL
+ channel_id: Optional channel ID to override the webhook's default channel
+ """
+ self.webhook_url = webhook_url
+ self.channel_id = channel_id
+ self.logger = logger.bind(component="DiscordWebhookClient")
+
+ def send_message(self, content: str, username: str = "UmaBot", avatar_url: Optional[str] = None) -> bool:
+ """Send a message to Discord.
+
+ Args:
+ content: The message content
+ username: The username to display (defaults to UmaBot)
+ avatar_url: Optional avatar URL
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ payload = {
+ "content": content,
+ "username": username
+ }
+
+ if avatar_url:
+ payload["avatar_url"] = avatar_url
+
+ if self.channel_id:
+ payload["channel_id"] = self.channel_id
+
+ response = requests.post(
+ self.webhook_url,
+ data=json.dumps(payload),
+ headers={"Content-Type": "application/json"},
+ timeout=10
+ )
+
+ if response.status_code == 204:
+ self.logger.debug("Discord message sent successfully")
+ return True
+ else:
+ self.logger.error(f"Failed to send Discord message: {response.status_code} - {response.text}")
+ return False
+
+ except Exception as e:
+ self.logger.error(f"Error sending Discord message: {e}")
+ return False
+
+ def send_embed(self, embed: dict, username: str = "UmaBot", avatar_url: Optional[str] = None) -> bool:
+ """Send an embed message to Discord.
+
+ Args:
+ embed: The embed object
+ username: The username to display (defaults to UmaBot)
+ avatar_url: Optional avatar URL
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ payload = {
+ "embeds": [embed],
+ "username": username
+ }
+
+ if avatar_url:
+ payload["avatar_url"] = avatar_url
+
+ if self.channel_id:
+ payload["channel_id"] = self.channel_id
+
+ response = requests.post(
+ self.webhook_url,
+ data=json.dumps(payload),
+ headers={"Content-Type": "application/json"},
+ timeout=10
+ )
+
+ if response.status_code == 204:
+ self.logger.debug("Discord embed sent successfully")
+ return True
+ else:
+ self.logger.error(f"Failed to send Discord embed: {response.status_code} - {response.text}")
+ return False
+
+ except Exception as e:
+ self.logger.error(f"Error sending Discord embed: {e}")
+ return False
+
+ def log_moderation_action(self, action: str, submission_id: str, author: str,
+ title: str, reason: Optional[str] = None,
+ post_url: Optional[str] = None) -> bool:
+ """Log a moderation action to Discord using an embed.
+
+ Args:
+ action: The action taken (e.g., "Removed", "Reflair to Art")
+ submission_id: The Reddit submission ID
+ author: The Reddit username
+ title: The post title
+ reason: Optional reason for the action
+ post_url: Optional URL to the post
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ # Determine color based on action
+ color = 0x00ff00 # Green for reflair
+ if "Removed" in action:
+ color = 0xff0000 # Red for removal
+
+ # Create embed
+ embed = {
+ "title": f"๐Ÿ”จ {action}",
+ "color": color,
+ "fields": [
+ {
+ "name": "๐Ÿ“ Post",
+ "value": title[:1024] if title else "No title", # Discord field limit
+ "inline": False
+ },
+ {
+ "name": "๐Ÿ‘ค Author",
+ "value": f"u/{author}",
+ "inline": True
+ },
+ {
+ "name": "๐Ÿ†” Post ID",
+ "value": submission_id,
+ "inline": True
+ }
+ ],
+ "timestamp": None, # Will be set to current time by Discord
+ "footer": {
+ "text": "UmaBot"
+ }
+ }
+
+ # Add reason field if provided
+ if reason:
+ embed["fields"].append({
+ "name": "๐Ÿ“‹ Reason",
+ "value": reason[:1024], # Discord field limit
+ "inline": False
+ })
+
+ # Add post URL if provided
+ if post_url:
+ embed["url"] = post_url
+
+ return self.send_embed(embed)
diff --git a/src/umabot/rules/intelligent_roleplay_moderator.py b/src/umabot/rules/intelligent_roleplay_moderator.py
index 865b4a4..50aa7d6 100644
--- a/src/umabot/rules/intelligent_roleplay_moderator.py
+++ b/src/umabot/rules/intelligent_roleplay_moderator.py
@@ -3,6 +3,7 @@
import praw.models
from .base import Rule
from .intelligent_moderator_base import IntelligentModeratorBase
+from ..discord_client import DiscordWebhookClient
class IntelligentRoleplayModerator(Rule, IntelligentModeratorBase):
@@ -13,6 +14,14 @@ class IntelligentRoleplayModerator(Rule, IntelligentModeratorBase):
Rule.__init__(self, config)
IntelligentModeratorBase.__init__(self, config.openai_api_key)
self.subreddit = subreddit
+
+ # Initialize Discord webhook client if configured
+ self.discord_client = None
+ if config.discord_webhook_url:
+ self.discord_client = DiscordWebhookClient(
+ webhook_url=config.discord_webhook_url,
+ channel_id=config.discord_log_channel_id
+ )
def should_remove(self, submission: praw.models.Submission) -> bool:
"""Evaluate a roleplay post and take appropriate action."""
@@ -130,6 +139,20 @@ class IntelligentRoleplayModerator(Rule, IntelligentModeratorBase):
submission.author.message(subject, message)
self.logger.info(f"Sent art flair change notification to {username}")
+ # Log to Discord if configured
+ if self.discord_client:
+ # Get AI reasoning for Discord log
+ ai_reason = evaluation.get('reasoning', 'Post appears to be primarily showcasing artwork')
+
+ self.discord_client.log_moderation_action(
+ action="Reflair to Art",
+ submission_id=submission.id,
+ author=username,
+ title=submission.title or "No title",
+ reason=ai_reason,
+ post_url=f"https://reddit.com{submission.permalink}"
+ )
+
except Exception as e:
self.logger.error(f"Error sending art flair notification for {submission.id}: {e}")
@@ -153,6 +176,22 @@ class IntelligentRoleplayModerator(Rule, IntelligentModeratorBase):
submission.author.message(subject, message)
self.logger.info(f"Sent low-effort removal notification to {username}")
+ # Log to Discord if configured
+ if self.discord_client:
+ # Get brief reason for Discord log
+ brief_reason = "Low-effort content"
+ if formatted_reasoning:
+ brief_reason = formatted_reasoning[:100] + "..." if len(formatted_reasoning) > 100 else formatted_reasoning
+
+ self.discord_client.log_moderation_action(
+ action="Removed",
+ submission_id=submission.id,
+ author=username,
+ title=submission.title or "No title",
+ reason=brief_reason,
+ post_url=f"https://reddit.com{submission.permalink}"
+ )
+
except Exception as e:
self.logger.error(f"Error sending low-effort notification for {submission.id}: {e}")
diff --git a/test_discord_embed.py b/test_discord_embed.py
new file mode 100755
index 0000000..0a9850f
--- /dev/null
+++ b/test_discord_embed.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+"""Test script for Discord embed functionality."""
+
+import os
+import sys
+from dotenv import load_dotenv
+
+# Add the src directory to the Python path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+from umabot.discord_client import DiscordWebhookClient
+
+def test_discord_embed():
+ """Test the Discord embed functionality."""
+ # Load environment variables
+ load_dotenv()
+
+ # Get Discord configuration
+ webhook_url = os.getenv("DISCORD_WEBHOOK_URL")
+ channel_id = os.getenv("DISCORD_LOG_CHANNEL_ID")
+
+ if not webhook_url:
+ print("โŒ Error: DISCORD_WEBHOOK_URL not found in environment variables")
+ print("Please set DISCORD_WEBHOOK_URL in your .env file")
+ return False
+
+ print(f"๐Ÿ”— Using webhook URL: {webhook_url[:50]}...")
+ if channel_id:
+ print(f"๐Ÿ“บ Using channel ID: {channel_id}")
+ else:
+ print("๐Ÿ“บ Using webhook's default channel")
+
+ # Initialize Discord client
+ discord_client = DiscordWebhookClient(webhook_url, channel_id)
+
+ print("\n๐Ÿงช Testing Discord embed functionality...")
+
+ # Test 1: Reflair action
+ print("\n1๏ธโƒฃ Testing reflair action embed...")
+ success1 = discord_client.log_moderation_action(
+ action="Reflair to Art",
+ submission_id="test123",
+ author="testuser",
+ title="Beautiful artwork of my favorite character!",
+ reason="The post contains primarily visual artwork content with minimal textual roleplay elements, focusing more on showcasing the artwork rather than engaging in character-driven narrative or dialogue.",
+ post_url="https://reddit.com/r/test/comments/test123/"
+ )
+
+ if success1:
+ print("โœ… Reflair embed sent successfully!")
+ else:
+ print("โŒ Failed to send reflair embed")
+
+ # Test 2: Removal action
+ print("\n2๏ธโƒฃ Testing removal action embed...")
+ success2 = discord_client.log_moderation_action(
+ action="Removed",
+ submission_id="test456",
+ author="anotheruser",
+ title="Short post with minimal content",
+ reason="Low-effort content, short length, no plot development, lacks creativity, minimal effort, no character development, poor quality content that doesn't meet community standards",
+ post_url="https://reddit.com/r/test/comments/test456/"
+ )
+
+ if success2:
+ print("โœ… Removal embed sent successfully!")
+ else:
+ print("โŒ Failed to send removal embed")
+
+ # Test 3: Long reason truncation (simulating real behavior)
+ print("\n3๏ธโƒฃ Testing long reason truncation...")
+ success3 = discord_client.log_moderation_action(
+ action="Removed",
+ submission_id="test789",
+ author="longreasonuser",
+ title="Another test post",
+ reason="This is a very long reason that should be truncated in the actual implementation because the intelligent moderator truncates reasons to 100 characters plus ellipsis when they are too long, just like this one which is definitely longer than 100 characters and should demonstrate the truncation behavior",
+ post_url="https://reddit.com/r/test/comments/test789/"
+ )
+
+ if success3:
+ print("โœ… Long reason embed sent successfully!")
+ else:
+ print("โŒ Failed to send long reason embed")
+
+ # Test 4: Custom embed
+ print("\n4๏ธโƒฃ Testing custom embed...")
+ custom_embed = {
+ "title": "๐Ÿงช Test Embed",
+ "description": "This is a test embed to verify the Discord webhook functionality.",
+ "color": 0x0099ff, # Blue color
+ "fields": [
+ {
+ "name": "๐Ÿ“Š Test Status",
+ "value": "โœ… All systems operational",
+ "inline": True
+ },
+ {
+ "name": "๐Ÿ”ง Bot Version",
+ "value": "UmaBot v1.0",
+ "inline": True
+ },
+ {
+ "name": "๐Ÿ“ Test Details",
+ "value": "This embed was generated by the test script to verify Discord webhook integration.",
+ "inline": False
+ }
+ ],
+ "footer": {
+ "text": "UmaBot Test Script"
+ }
+ }
+
+ success4 = discord_client.send_embed(custom_embed)
+
+ if success4:
+ print("โœ… Custom embed sent successfully!")
+ else:
+ print("โŒ Failed to send custom embed")
+
+ # Summary
+ print("\n๐Ÿ“Š Test Summary:")
+ print(f" Reflair embed: {'โœ… Success' if success1 else 'โŒ Failed'}")
+ print(f" Removal embed: {'โœ… Success' if success2 else 'โŒ Failed'}")
+ print(f" Long reason embed: {'โœ… Success' if success3 else 'โŒ Failed'}")
+ print(f" Custom embed: {'โœ… Success' if success4 else 'โŒ Failed'}")
+
+ total_success = sum([success1, success2, success3, success4])
+ print(f"\n๐ŸŽฏ Total: {total_success}/4 tests passed")
+
+ if total_success == 4:
+ print("๐ŸŽ‰ All tests passed! Discord embed functionality is working correctly.")
+ return True
+ else:
+ print("โš ๏ธ Some tests failed. Check your Discord webhook configuration.")
+ return False
+
+def main():
+ """Main function."""
+ print("๐Ÿค– UmaBot Discord Embed Test Script")
+ print("=" * 50)
+
+ try:
+ success = test_discord_embed()
+ sys.exit(0 if success else 1)
+ except KeyboardInterrupt:
+ print("\n\nโน๏ธ Test interrupted by user")
+ sys.exit(1)
+ except Exception as e:
+ print(f"\nโŒ Unexpected error: {e}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()