Initial commit
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
FROM python:3.14-slim
|
||||
|
||||
# Copy application files
|
||||
WORKDIR /app
|
||||
COPY config.py .
|
||||
COPY flairtimermodmail.py .
|
||||
|
||||
RUN mkdir -p /app/config
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir praw
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run the script
|
||||
CMD ["python", "flairtimermodmail.py"]
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# FlairTimerModMail
|
||||
|
||||
FlairTimerModMail is a Reddit bot designed to monitor new posts in a specified subreddit for a particular flair. When a post receives the target flair, the bot starts a timer. If the flair remains on the post for a configurable number of hours, the bot sends a modmail notification to the subreddit moderators. This tool is useful for subreddits that use flairs to track posts requiring moderator attention or follow-up.
|
||||
|
||||
## Features
|
||||
- Monitors new posts in a subreddit for a specific flair
|
||||
- Tracks how long a post has had the flair
|
||||
- Sends a modmail notification after a configurable time period
|
||||
- Configurable via environment variables or a config file
|
||||
- Supports running natively with Python or in Docker (standalone or with Docker Compose)
|
||||
|
||||
## Requirements
|
||||
- Python 3.8+
|
||||
- Reddit API credentials (see below)
|
||||
|
||||
## Configuration
|
||||
The bot can be configured using a `config/config.py` file or via environment variables. When running in Docker, environment variables are recommended and can be managed with a `.env` file.
|
||||
|
||||
### Required Configuration Values
|
||||
- `USERNAME`: Reddit username
|
||||
- `PASSWORD`: Reddit password
|
||||
- `CLIENT_ID`: Reddit API client ID
|
||||
- `CLIENT_SECRET`: Reddit API client secret
|
||||
- `USER_AGENT`: User agent string for Reddit API
|
||||
- `SUBREDDIT`: Subreddit to monitor
|
||||
- `FLAIR_TEXT`: Flair text to track (case sensitive)
|
||||
- `INTERVAL`: How often to scan (seconds)
|
||||
- `HOURS`: How many hours before sending notification
|
||||
- `MESSAGETITLE`: Title for the modmail message
|
||||
- `SEARCHLIMIT`: How many posts to scan (max 1000)
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
### 1. Python (Native)
|
||||
1. Clone the repository:
|
||||
```sh
|
||||
git clone https://github.com/slfhstd/FlairTimerModMail.git
|
||||
cd FlairTimerModMail
|
||||
```
|
||||
2. Install dependencies:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Copy `example.env` to `.env` and fill in your Reddit credentials and settings, or edit `config/config.py` directly.
|
||||
4. Run the bot:
|
||||
```sh
|
||||
python flairtimermodmail.py
|
||||
```
|
||||
|
||||
### 2. Docker Run
|
||||
1. Copy `example.env` to `.env` and fill in your values.
|
||||
2. Run the bot with Docker:
|
||||
```sh
|
||||
docker run --env-file .env -v $(pwd)/config:/app/config ghcr.io/slfhstd/flairtimermodmail:latest
|
||||
```
|
||||
|
||||
### 3. Docker Compose
|
||||
1. Clone the repository:
|
||||
```sh
|
||||
git clone https://github.com/slfhstd/FlairTimerModMail.git
|
||||
cd FlairTimerModMail
|
||||
```
|
||||
2. Copy `example.env` to `.env` and fill in your values.
|
||||
3. Start the bot with Docker Compose:
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The bot will auto-generate a config file from environment variables if one does not exist.
|
||||
- Make sure your Reddit account has the necessary permissions and API credentials.
|
||||
- For production use, keep your credentials secure and do not commit `.env` files with real secrets to version control.
|
||||
|
||||
## License
|
||||
This project is provided as-is under the MIT License.
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Robust shim for project configuration.
|
||||
|
||||
This file attempts to locate a real configuration module named either
|
||||
- a package module at `config/config.py` (importable as `config.config`), or
|
||||
- a standalone `config.py` file elsewhere on `sys.path`.
|
||||
|
||||
It then imports that module and re-exports its public names so existing
|
||||
`import config` usages continue to work regardless of whether the
|
||||
configuration was moved into a `config/` directory or left as a file.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import importlib
|
||||
import importlib.util
|
||||
|
||||
|
||||
def _find_real_config():
|
||||
# Try the straightforward import first (works when `config` is a package)
|
||||
try:
|
||||
return importlib.import_module('config.config')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Search sys.path for a candidate `config/config.py` or another `config.py` (not this file)
|
||||
this_path = os.path.abspath(__file__)
|
||||
for p in sys.path:
|
||||
if not p:
|
||||
p = os.getcwd()
|
||||
# candidate package file
|
||||
cand = os.path.join(p, 'config', 'config.py')
|
||||
if os.path.isfile(cand):
|
||||
spec = importlib.util.spec_from_file_location('config_real', cand)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
# candidate standalone config.py (avoid loading this shim file)
|
||||
cand2 = os.path.join(p, 'config.py')
|
||||
if os.path.isfile(cand2) and os.path.abspath(cand2) != this_path:
|
||||
spec = importlib.util.spec_from_file_location('config_real', cand2)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_real = _find_real_config()
|
||||
if _real is None:
|
||||
raise ImportError('Could not locate real configuration (config/config.py or another config.py)')
|
||||
|
||||
# Re-export public names from the real config module
|
||||
for _k, _v in _real.__dict__.items():
|
||||
if not _k.startswith('_'):
|
||||
globals()[_k] = _v
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
username = ""
|
||||
password = ""
|
||||
client_id = ""
|
||||
client_secret = ""
|
||||
user_agent = "Flair Timer Mod Mail Bot"
|
||||
|
||||
#Subreddits
|
||||
subreddit = "" # "INEEEEDIT" "Ofcoursethatsathing" "All"
|
||||
|
||||
flair_text = "" # Case Sensitive
|
||||
|
||||
interval = 30 # How often should the bot scan the subreddit for these posts, in seconds. Higher = slower/less accurate/save resources, lower = faster/more accurate/use more resources.
|
||||
|
||||
hours = 0.17 # How many hours must the flair been on the post to send the notification
|
||||
|
||||
messagetitle = "" # Title of the modmail
|
||||
searchlimit = 900 # Max: 1000, this should only be limited to save on resources. The bot sorts by new and if it isn't catching posts that are being changed to the flair simply because they are too old (say the 301st post on the subreddit is changed to the flair) then increase this limit.his limit.
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/slfhstd/flairtimermodmail:v0.0.1
|
||||
container_name: bot-ftmm-dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /docker/data/flairtimermodmail:/app/config
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Example environment variables for FlairTimerModMail
|
||||
USERNAME=your_reddit_username
|
||||
PASSWORD=your_reddit_password
|
||||
CLIENT_ID=your_reddit_client_id
|
||||
CLIENT_SECRET=your_reddit_client_secret
|
||||
USER_AGENT=Flair Timer Mod Mail Bot
|
||||
SUBREDDIT=your_subreddit
|
||||
FLAIR_TEXT=Waiting for OP
|
||||
INTERVAL=30
|
||||
HOURS=48
|
||||
MESSAGETITLE=Modmail Notification
|
||||
SEARCHLIMIT=600
|
||||
@@ -0,0 +1,108 @@
|
||||
import praw
|
||||
import os.path
|
||||
import json
|
||||
import time
|
||||
|
||||
# Create default config/config.py if it doesn't exist and exit to prompt manual editing
|
||||
import sys
|
||||
|
||||
default_config_path = os.path.join('config', 'config.py')
|
||||
def env_or_default(var, default):
|
||||
return os.environ.get(var, default)
|
||||
|
||||
def write_config_from_env():
|
||||
os.makedirs('config', exist_ok=True)
|
||||
with open(default_config_path, 'w') as f:
|
||||
f.write(
|
||||
f'username = "{env_or_default("USERNAME", "")}"\n'
|
||||
f'password = "{env_or_default("PASSWORD", "")}"\n'
|
||||
f'client_id = "{env_or_default("CLIENT_ID", "")}"\n'
|
||||
f'client_secret = "{env_or_default("CLIENT_SECRET", "")}"\n'
|
||||
f'user_agent = "{env_or_default("USER_AGENT", "Flair Timer Comment Bot" )}"\n'
|
||||
'\n'
|
||||
f'subreddit = "{env_or_default("SUBREDDIT", "")}"\n'
|
||||
f'flair_text = "{env_or_default("FLAIR_TEXT", "Waiting for OP")}"\n'
|
||||
f'interval = {env_or_default("INTERVAL", "30")}\n'
|
||||
f'hours = {env_or_default("HOURS", "48")}\n'
|
||||
f'messagetitle = "{env_or_default("MESSAGETITLE", "Modmail Notification")}"\n'
|
||||
f'searchlimit = {env_or_default("SEARCHLIMIT", "600")}\n'
|
||||
)
|
||||
print(f"Configuration file auto-populated from environment variables at {default_config_path}.")
|
||||
|
||||
# Check if config file is missing or empty
|
||||
populate_config = False
|
||||
if not os.path.exists(default_config_path):
|
||||
populate_config = True
|
||||
else:
|
||||
try:
|
||||
with open(default_config_path, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if not content:
|
||||
populate_config = True
|
||||
except Exception:
|
||||
populate_config = True
|
||||
|
||||
if populate_config:
|
||||
write_config_from_env()
|
||||
sys.exit(0)
|
||||
|
||||
if not os.path.exists(default_config_path):
|
||||
sys.exit(0)
|
||||
import config
|
||||
|
||||
def authentication():
|
||||
print ("Authenticating...")
|
||||
reddit = praw.Reddit(username = config.username,
|
||||
password = config.password,
|
||||
client_id = config.client_id,
|
||||
client_secret = config.client_secret,
|
||||
user_agent = config.user_agent)
|
||||
print ("Authenticated as {}.".format(reddit.user.me()))
|
||||
return reddit
|
||||
|
||||
def main(reddit, posts: dict):
|
||||
while True:
|
||||
for submission in reddit.subreddit(config.subreddit).new(limit=config.searchlimit):
|
||||
if not submission.saved:
|
||||
if submission.id not in posts.keys() and submission.link_flair_text == config.flair_text:
|
||||
posts[submission.id] = time.time()
|
||||
print(f"Post {submission} has been flaired {config.flair_text}")
|
||||
if submission.id in posts.keys() and submission.link_flair_text != config.flair_text:
|
||||
posts.pop(submission.id)
|
||||
print(f"Post {submission} has been unflaired {config.flair_text}")
|
||||
|
||||
for submission in posts:
|
||||
if time.time() > posts[submission] + (config.hours * 60 * 60):
|
||||
posts.pop(submission)
|
||||
reddit.submission(submission).save()
|
||||
data = {
|
||||
"subject": config.messagetitle,
|
||||
"text": f"It has been {config.hours/24} day/s since this was flaired [{config.flair_text}](https://old.reddit.com{reddit.submission(submission).permalink})",
|
||||
"to": "/r/{}".format(config.subreddit),
|
||||
}
|
||||
reddit.post("api/compose/", data=data)
|
||||
print(f"Post {submission} has been flaired {config.flair_text} for {config.hours * 60} minutes, sent modmail")
|
||||
break
|
||||
|
||||
save_posts(posts)
|
||||
time.sleep(config.interval)
|
||||
|
||||
def load_posts():
|
||||
if not os.path.exists("config/posts.json"):
|
||||
with open("config/posts.json", "w+") as file:
|
||||
json.dump({}, file)
|
||||
with open("config/posts.json", "r+") as file:
|
||||
data = json.load(file)
|
||||
return data
|
||||
|
||||
def save_posts(data):
|
||||
with open('config/posts.json', 'w+') as file:
|
||||
json.dump(data, file)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
posts = load_posts()
|
||||
main(reddit = authentication(), posts = posts)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
Reference in New Issue
Block a user