diff options
| author | Fuwn <[email protected]> | 2025-09-15 23:40:34 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-15 23:40:34 -0700 |
| commit | 699966bd2d26683dc20c6d0bd42598f54d786af0 (patch) | |
| tree | 4b2aaa7d26e0505e52f78a2166e7bfe3de57db0c | |
| parent | feat(roleplay_limiter): Add system notice to removal message (diff) | |
| download | umabot-699966bd2d26683dc20c6d0bd42598f54d786af0.tar.xz umabot-699966bd2d26683dc20c6d0bd42598f54d786af0.zip | |
feat(spam_detector): Use UTC time as limit reset time
| -rw-r--r-- | README.md | 44 | ||||
| -rw-r--r-- | env.example | 1 | ||||
| -rw-r--r-- | render.yaml | 2 | ||||
| -rw-r--r-- | src/umabot/config.py | 5 | ||||
| -rw-r--r-- | src/umabot/rules/spam_detector.py | 51 | ||||
| -rw-r--r-- | tests/test_config.py | 4 |
6 files changed, 58 insertions, 49 deletions
@@ -16,14 +16,16 @@ A modular Reddit bot for automated post moderation built with Python and PRAW. The bot supports two roleplay limiting modes: #### Surge-Based Limiting (Default) + Intelligent surge detection that adjusts limits based on subreddit activity: - **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 +- **High Surge** (40+ roleplay posts): Users limited to 1 roleplay post per time window - **Extreme Surge** (60+ roleplay posts): All roleplay posts temporarily blocked #### Static Limiting (Optional) + Fixed limit that doesn't change based on activity: - **Fixed Limit**: Users can post a configurable number of roleplay posts per time window (default: 1) @@ -72,7 +74,6 @@ REDDIT_USER_AGENT=UmaBot/0.1.0 SUBREDDIT_NAME=your_subreddit_name # Bot Messages -SPAM_MESSAGE=Your post has been removed for posting too frequently. Please wait before posting again. ROLEPLAY_MESSAGE=Your post has been removed. Only one roleplay post is allowed per user. # Bot Settings @@ -127,7 +128,7 @@ class MyCustomRule(Rule): def should_remove(self, submission): # Your logic here return False - + def get_removal_message(self, submission): return "Your post was removed for violating our custom rule." ``` @@ -170,25 +171,24 @@ The bot can be deployed on any platform that supports Python: ### Environment Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `REDDIT_CLIENT_ID` | Reddit API client ID | Required | -| `REDDIT_CLIENT_SECRET` | Reddit API client secret | Required | -| `REDDIT_USERNAME` | Reddit bot username | Required | -| `REDDIT_PASSWORD` | Reddit bot password | Required | -| `REDDIT_USER_AGENT` | User agent string | `UmaBot/0.1.0` | -| `SUBREDDIT_NAME` | Target subreddit name | Required | -| `SPAM_MESSAGE` | Message for spam removals | Customizable | -| `ROLEPLAY_MESSAGE` | Message for roleplay removals | Customizable | -| `CHECK_INTERVAL` | Seconds between checks | `60` | -| `MAX_POSTS_PER_DAY` | Max posts per user in time window | `3` | -| `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` | +| Variable | Description | Default | +| ----------------------------- | ------------------------------------------ | -------------- | +| `REDDIT_CLIENT_ID` | Reddit API client ID | Required | +| `REDDIT_CLIENT_SECRET` | Reddit API client secret | Required | +| `REDDIT_USERNAME` | Reddit bot username | Required | +| `REDDIT_PASSWORD` | Reddit bot password | Required | +| `REDDIT_USER_AGENT` | User agent string | `UmaBot/0.1.0` | +| `SUBREDDIT_NAME` | Target subreddit name | Required | +| `ROLEPLAY_MESSAGE` | Message for roleplay removals | Customizable | +| `CHECK_INTERVAL` | Seconds between checks | `60` | +| `MAX_POSTS_PER_DAY` | Max posts per user in time window | `3` | +| `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 be010ee..9591db2 100644 --- a/env.example +++ b/env.example @@ -12,7 +12,6 @@ SUBREDDIT_NAME=your_subreddit_name # Bot Messages # Customize these messages as needed -SPAM_MESSAGE=Your post has been removed for posting too frequently. Please wait before posting again. ROLEPLAY_MESSAGE=Your post has been removed. Only one roleplay post is allowed per user. # Bot Settings diff --git a/render.yaml b/render.yaml index 86b706d..9bfbd78 100644 --- a/render.yaml +++ b/render.yaml @@ -37,7 +37,5 @@ services: value: "60" - key: DRY_RUN value: "false" - - key: SPAM_MESSAGE - value: "Your post has been removed for posting too frequently. Please wait before posting again." - key: ROLEPLAY_MESSAGE value: "Your post has been removed. Only one roleplay post is allowed per user." diff --git a/src/umabot/config.py b/src/umabot/config.py index f86bed5..7f635f1 100644 --- a/src/umabot/config.py +++ b/src/umabot/config.py @@ -23,7 +23,6 @@ class Config: subreddit_name: str # Bot messages - spam_message: str roleplay_message: str # Bot settings @@ -49,10 +48,6 @@ class Config: password=os.getenv("REDDIT_PASSWORD", ""), user_agent=os.getenv("REDDIT_USER_AGENT", "UmaBot/0.1.0"), subreddit_name=os.getenv("SUBREDDIT_NAME", ""), - spam_message=os.getenv( - "SPAM_MESSAGE", - "Your post has been removed for posting too frequently. Please wait before posting again." - ), roleplay_message=os.getenv( "ROLEPLAY_MESSAGE", "Your post has been removed. Only one roleplay post is allowed per user." diff --git a/src/umabot/rules/spam_detector.py b/src/umabot/rules/spam_detector.py index 4411fc8..3fccf0a 100644 --- a/src/umabot/rules/spam_detector.py +++ b/src/umabot/rules/spam_detector.py @@ -1,7 +1,7 @@ """Spam detection rule for limiting posts per user per day.""" import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, List import praw.models from .base import Rule @@ -13,9 +13,8 @@ class SpamDetector(Rule): def __init__(self, config): """Initialize the spam detector.""" super().__init__(config) - self.user_posts: Dict[str, List[float]] = {} + self.user_posts: Dict[str, List[datetime]] = {} self.max_posts = config.max_posts_per_day - self.time_window = config.post_limit_window_hours * 60 * 60 # Convert hours to seconds def should_remove(self, submission: praw.models.Submission) -> bool: """Check if a user has posted too frequently.""" @@ -23,24 +22,24 @@ class SpamDetector(Rule): return False username = submission.author.name - current_time = time.time() + current_utc = datetime.now(timezone.utc) - # Clean old posts from tracking - self._clean_old_posts(username, current_time) + # Clean old posts from tracking (remove posts from previous days) + self._clean_old_posts(username, current_utc) - # Count current posts in the time window + # Count current posts in today's UTC day if username not in self.user_posts: self.user_posts[username] = [] post_count = len(self.user_posts[username]) # Add current post to tracking - self.user_posts[username].append(current_time) + self.user_posts[username].append(current_utc) # Check if this post exceeds the limit if post_count >= self.max_posts: self.logger.info( - f"User {username} has posted {post_count + 1} times in {self.config.post_limit_window_hours} hours " + f"User {username} has posted {post_count + 1} times today (UTC) " f"(limit: {self.max_posts})" ) return True @@ -48,16 +47,38 @@ class SpamDetector(Rule): return False def get_removal_message(self, submission: praw.models.Submission) -> str: - """Get the spam removal message.""" - return self.config.spam_message + """Get the spam removal message with time remaining until limit expires.""" + username = submission.author.name if submission.author else "Unknown" + current_utc = datetime.now(timezone.utc) + + # Calculate time until next UTC day (midnight UTC) + next_day = current_utc.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + time_remaining = next_day - current_utc + + # Format time remaining + hours = int(time_remaining.total_seconds() // 3600) + minutes = int((time_remaining.total_seconds() % 3600) // 60) + + if hours > 0: + time_str = f"{hours}h {minutes}m" + else: + time_str = f"{minutes}m" + + return ( + f"Your post has been removed. Users in r/{self.config.subreddit_name} can submit up to " + f"{self.max_posts} posts per day (UTC). Your limit resets in {time_str}." + ) - def _clean_old_posts(self, username: str, current_time: float) -> None: - """Remove posts older than the time window from tracking.""" + def _clean_old_posts(self, username: str, current_utc: datetime) -> None: + """Remove posts from previous UTC days from tracking.""" if username not in self.user_posts: return - cutoff_time = current_time - self.time_window + # Get start of current UTC day + today_start = current_utc.replace(hour=0, minute=0, second=0, microsecond=0) + + # Keep only posts from today self.user_posts[username] = [ post_time for post_time in self.user_posts[username] - if post_time > cutoff_time + if post_time >= today_start ] diff --git a/tests/test_config.py b/tests/test_config.py index 30ccda7..a160a84 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,7 +15,6 @@ def test_config_from_env(): "REDDIT_USERNAME": "test_username", "REDDIT_PASSWORD": "test_password", "SUBREDDIT_NAME": "test_subreddit", - "SPAM_MESSAGE": "Test spam message", "ROLEPLAY_MESSAGE": "Test roleplay message", } @@ -27,7 +26,6 @@ def test_config_from_env(): assert config.username == "test_username" assert config.password == "test_password" assert config.subreddit_name == "test_subreddit" - assert config.spam_message == "Test spam message" assert config.roleplay_message == "Test roleplay message" assert config.check_interval == 60 assert config.max_posts_per_day == 3 @@ -43,7 +41,6 @@ def test_config_validation(): password="", user_agent="test", subreddit_name="", - spam_message="", roleplay_message="" ) @@ -60,7 +57,6 @@ def test_config_validation_success(): password="test", user_agent="test", subreddit_name="test", - spam_message="test", roleplay_message="test" ) |