aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-15 23:40:34 -0700
committerFuwn <[email protected]>2025-09-15 23:40:34 -0700
commit699966bd2d26683dc20c6d0bd42598f54d786af0 (patch)
tree4b2aaa7d26e0505e52f78a2166e7bfe3de57db0c
parentfeat(roleplay_limiter): Add system notice to removal message (diff)
downloadumabot-699966bd2d26683dc20c6d0bd42598f54d786af0.tar.xz
umabot-699966bd2d26683dc20c6d0bd42598f54d786af0.zip
feat(spam_detector): Use UTC time as limit reset time
-rw-r--r--README.md44
-rw-r--r--env.example1
-rw-r--r--render.yaml2
-rw-r--r--src/umabot/config.py5
-rw-r--r--src/umabot/rules/spam_detector.py51
-rw-r--r--tests/test_config.py4
6 files changed, 58 insertions, 49 deletions
diff --git a/README.md b/README.md
index 12f8639..f6dd88a 100644
--- a/README.md
+++ b/README.md
@@ -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"
)