aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-03 21:13:51 -0700
committerFuwn <[email protected]>2025-09-03 21:13:51 -0700
commit6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092 (patch)
tree933b556822fa3819992a243efb91202890db0992
parentfeat: Add user-specified post limit window (diff)
downloadumabot-6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092.tar.xz
umabot-6b9d23965c2c865b7e5ffd5ffb0fbf6b24dae092.zip
feat(roleplay_limiter): Use surge-based removal model
-rw-r--r--README.md16
-rw-r--r--env.example4
-rw-r--r--render.yaml6
-rw-r--r--src/umabot/bot.py2
-rw-r--r--src/umabot/config.py8
-rw-r--r--src/umabot/rules/roleplay_limiter.py86
6 files changed, 104 insertions, 18 deletions
diff --git a/README.md b/README.md
index 6b3641b..90621ac 100644
--- a/README.md
+++ b/README.md
@@ -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