diff options
| author | Fuwn <[email protected]> | 2025-09-03 21:13:51 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-03 21:13:51 -0700 |
| commit | 6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092 (patch) | |
| tree | 933b556822fa3819992a243efb91202890db0992 | |
| parent | feat: Add user-specified post limit window (diff) | |
| download | umabot-6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092.tar.xz umabot-6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092.zip | |
feat(roleplay_limiter): Use surge-based removal model
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | env.example | 4 | ||||
| -rw-r--r-- | render.yaml | 6 | ||||
| -rw-r--r-- | src/umabot/bot.py | 2 | ||||
| -rw-r--r-- | src/umabot/config.py | 8 | ||||
| -rw-r--r-- | src/umabot/rules/roleplay_limiter.py | 86 |
6 files changed, 104 insertions, 18 deletions
@@ -5,12 +5,23 @@ A modular Reddit bot for automated post moderation built with Python and PRAW. ## Features - **Spam Detection**: Automatically removes posts from users who post more than 3 times in 24 hours -- **Roleplay Limiter**: Prevents users from posting multiple times with the "Roleplay" flair +- **Surge-Based Roleplay Limiter**: Dynamically adjusts roleplay post limits based on subreddit activity - **Modular Design**: Easy to add new moderation rules - **Configurable Messages**: Customizable removal messages - **Dry Run Mode**: Test the bot without actually removing posts - **Comprehensive Logging**: Detailed logs for monitoring and debugging +### Surge-Based Roleplay Limiting + +The bot uses intelligent surge detection to manage roleplay posts: + +- **Normal Activity** (< 20 roleplay posts): Users can post up to 5 roleplay posts per time window +- **Moderate Surge** (20+ roleplay posts): Users limited to 3 roleplay posts per time window +- **High Surge** (40+ roleplay posts): Users limited to 1 roleplay post per time window +- **Extreme Surge** (60+ roleplay posts): All roleplay posts temporarily blocked + +The system automatically adjusts limits based on recent roleplay activity and provides dynamic removal messages explaining the current restrictions. + ## Quick Start ### 1. Install Dependencies @@ -164,6 +175,9 @@ The bot can be deployed on any platform that supports Python: | `MAX_ROLEPLAY_POSTS_PER_DAY` | Max roleplay posts per user in time window | `1` | | `POST_LIMIT_WINDOW_HOURS` | Time window for post limits (hours) | `24` | | `ROLEPLAY_LIMIT_WINDOW_HOURS` | Time window for roleplay limits (hours) | `24` | +| `ROLEPLAY_SURGE_THRESHOLD_1` | First surge threshold for roleplay posts | `20` | +| `ROLEPLAY_SURGE_THRESHOLD_2` | Second surge threshold for roleplay posts | `40` | +| `ROLEPLAY_SURGE_THRESHOLD_3` | Third surge threshold for roleplay posts | `60` | | `DRY_RUN` | Enable dry-run mode | `false` | ## Development diff --git a/env.example b/env.example index 9ed779a..be010ee 100644 --- a/env.example +++ b/env.example @@ -26,5 +26,9 @@ MAX_ROLEPLAY_POSTS_PER_DAY=1 POST_LIMIT_WINDOW_HOURS=24 # Time window for roleplay post limits (in hours) ROLEPLAY_LIMIT_WINDOW_HOURS=24 +# Surge-based roleplay limiting thresholds +ROLEPLAY_SURGE_THRESHOLD_1=20 +ROLEPLAY_SURGE_THRESHOLD_2=40 +ROLEPLAY_SURGE_THRESHOLD_3=60 # Set to true to test without actually removing posts DRY_RUN=false diff --git a/render.yaml b/render.yaml index cb46cef..86b706d 100644 --- a/render.yaml +++ b/render.yaml @@ -29,6 +29,12 @@ services: value: "24" - key: ROLEPLAY_LIMIT_WINDOW_HOURS value: "24" + - key: ROLEPLAY_SURGE_THRESHOLD_1 + value: "20" + - key: ROLEPLAY_SURGE_THRESHOLD_2 + value: "40" + - key: ROLEPLAY_SURGE_THRESHOLD_3 + value: "60" - key: DRY_RUN value: "false" - key: SPAM_MESSAGE diff --git a/src/umabot/bot.py b/src/umabot/bot.py index 13f3a84..6c9797c 100644 --- a/src/umabot/bot.py +++ b/src/umabot/bot.py @@ -75,7 +75,7 @@ class UmaBot: # Initialize rules self.rules = [ SpamDetector(config), - RoleplayLimiter(config) + RoleplayLimiter(config, self.subreddit) ] # Track processed submissions to avoid processing old posts diff --git a/src/umabot/config.py b/src/umabot/config.py index f1cdf92..8748693 100644 --- a/src/umabot/config.py +++ b/src/umabot/config.py @@ -34,6 +34,11 @@ class Config: roleplay_limit_window_hours: int = 24 # hours dry_run: bool = False + # Surge-based roleplay limiting + roleplay_surge_threshold_1: int = 20 # First threshold for surge detection + roleplay_surge_threshold_2: int = 40 # Second threshold for surge detection + roleplay_surge_threshold_3: int = 60 # Third threshold for surge detection + @classmethod def from_env(cls) -> "Config": """Create configuration from environment variables.""" @@ -58,6 +63,9 @@ class Config: post_limit_window_hours=int(os.getenv("POST_LIMIT_WINDOW_HOURS", "24")), roleplay_limit_window_hours=int(os.getenv("ROLEPLAY_LIMIT_WINDOW_HOURS", "24")), dry_run=os.getenv("DRY_RUN", "false").lower() == "true", + roleplay_surge_threshold_1=int(os.getenv("ROLEPLAY_SURGE_THRESHOLD_1", "20")), + roleplay_surge_threshold_2=int(os.getenv("ROLEPLAY_SURGE_THRESHOLD_2", "40")), + roleplay_surge_threshold_3=int(os.getenv("ROLEPLAY_SURGE_THRESHOLD_3", "60")), ) def validate(self) -> None: diff --git a/src/umabot/rules/roleplay_limiter.py b/src/umabot/rules/roleplay_limiter.py index f7724e2..60490a9 100644 --- a/src/umabot/rules/roleplay_limiter.py +++ b/src/umabot/rules/roleplay_limiter.py @@ -1,4 +1,4 @@ -"""Roleplay post limiter rule.""" +"""Surge-based roleplay post limiter rule.""" import time from typing import Dict, List @@ -7,18 +7,26 @@ from .base import Rule class RoleplayLimiter(Rule): - """Limits users to a configurable number of roleplay posts per day.""" + """Surge-based roleplay limiter that adjusts limits based on subreddit activity.""" - def __init__(self, config): - """Initialize the roleplay limiter.""" + def __init__(self, config, subreddit): + """Initialize the surge-based roleplay limiter.""" super().__init__(config) + self.subreddit = subreddit self.user_roleplay_posts: Dict[str, List[float]] = {} - self.max_roleplay_posts = config.max_roleplay_posts_per_day - self.time_window = config.roleplay_limit_window_hours * 60 * 60 # Convert hours to seconds + self.surge_window = config.roleplay_limit_window_hours * 60 * 60 # Convert hours to seconds self.roleplay_flair = "Roleplay" + + # Surge-based limits configuration + self.base_limit = 5 # Base limit when no surge (maximum posts per user) + self.surge_thresholds = [ + (config.roleplay_surge_threshold_1, 3), # First threshold = 3 posts per user + (config.roleplay_surge_threshold_2, 1), # Second threshold = 1 post per user + (config.roleplay_surge_threshold_3, 0), # Third threshold = 0 posts per user (block all) + ] def should_remove(self, submission: praw.models.Submission) -> bool: - """Check if a user has posted too many roleplay posts.""" + """Check if a user has posted too many roleplay posts based on surge.""" if not submission.author: return False @@ -29,38 +37,84 @@ class RoleplayLimiter(Rule): username = submission.author.name current_time = time.time() + # Get current surge level and user limit + surge_level, user_limit = self._get_surge_level_and_limit() + # Clean old posts from tracking self._clean_old_posts(username, current_time) - # Count current roleplay posts in the time window + # Count current roleplay posts for this user if username not in self.user_roleplay_posts: self.user_roleplay_posts[username] = [] - post_count = len(self.user_roleplay_posts[username]) + user_post_count = len(self.user_roleplay_posts[username]) # Add current post to tracking self.user_roleplay_posts[username].append(current_time) - # Check if this post exceeds the limit - if post_count >= self.max_roleplay_posts: + # Check if this post exceeds the user's limit + if user_post_count >= user_limit: self.logger.info( - f"User {username} has posted {post_count + 1} roleplay posts in {self.config.roleplay_limit_window_hours} hours " - f"(limit: {self.max_roleplay_posts})" + f"User {username} has posted {user_post_count + 1} roleplay posts " + f"(limit: {user_limit} due to surge level: {surge_level} roleplay posts in {self.config.roleplay_limit_window_hours}h)" ) return True return False def get_removal_message(self, submission: praw.models.Submission) -> str: - """Get the roleplay removal message.""" - return self.config.roleplay_message + """Get the dynamic roleplay removal message.""" + surge_level, user_limit = self._get_surge_level_and_limit() + + if user_limit == 0: + return ( + f"Your post has been removed. Due to high roleplay activity " + f"({surge_level} roleplay posts in the last {self.config.roleplay_limit_window_hours} hours), " + f"roleplay posts are temporarily restricted in r/{self.config.subreddit_name}." + ) + else: + return ( + f"Your post has been removed. Due to high roleplay activity " + f"({surge_level} roleplay posts in the last {self.config.roleplay_limit_window_hours} hours), " + f"users in r/{self.config.subreddit_name} can submit {user_limit} roleplay post(s) " + f"within a {self.config.roleplay_limit_window_hours}-hour time window." + ) + + def _get_surge_level_and_limit(self) -> tuple[int, int]: + """Get current surge level and corresponding user limit.""" + try: + # Get recent roleplay posts from subreddit + current_time = time.time() + cutoff_time = current_time - self.surge_window + + # Count roleplay posts in the surge window + surge_count = 0 + for submission in self.subreddit.new(limit=100): + if submission.created_utc < cutoff_time: + break + if self._is_roleplay_post(submission): + surge_count += 1 + + # Determine limit based on surge level + user_limit = self.base_limit + for threshold, limit in self.surge_thresholds: + if surge_count >= threshold: + user_limit = limit + else: + break + + return surge_count, user_limit + + except Exception as e: + self.logger.error(f"Error calculating surge level: {e}") + return 0, self.base_limit def _clean_old_posts(self, username: str, current_time: float) -> None: """Remove roleplay posts older than the time window from tracking.""" if username not in self.user_roleplay_posts: return - cutoff_time = current_time - self.time_window + cutoff_time = current_time - self.surge_window self.user_roleplay_posts[username] = [ post_time for post_time in self.user_roleplay_posts[username] if post_time > cutoff_time |