6 Commits

Author SHA1 Message Date
slfhstd 5496ae64b6 Merge pull request 'MultiFlairTest' (#2) from MultiFlairTest into main
Reviewed-on: #2
2026-03-05 18:56:17 +00:00
slfhstd 818df56222 Final changes before merge 2026-03-05 18:53:30 +00:00
slfhstd 1e22463db0 name change 2026-03-05 17:16:28 +00:00
slfhstd 5dc2098581 Updates to docker configs 2026-03-05 17:07:19 +00:00
slfhstd 5af4311719 config via docker environment variables 2026-02-25 22:18:02 +00:00
slfhstd 2ef948e970 Auto create config file if it doesn't exist 2026-02-25 20:27:54 +00:00
5 changed files with 202 additions and 74 deletions
+28 -19
View File
@@ -1,27 +1,36 @@
# Bot authentication and global settings
username = "" username = ""
password = "" password = ""
client_id = "" client_id = ""
client_secret = "" client_secret = ""
user_agent = "Flair Timer Comment Bot" user_agent = "Flair Timer Comment Bot"
#Subreddits
subreddit = "" # "INEEEEDIT" "Ofcoursethatsathing" "All"
flair_text = "Waiting for OP" # 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 = 48 # How many hours must the flair been on the post to send the notification
searchlimit = 600 # 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.
# Comment message to post on old posts # Subreddit to monitor
comment_message = "" subreddit = "" # e.g. "INEEEEDIT", "Ofcoursethatsathing", "All"
# Whether the bot should lock the post after posting the comment (True/False) # How often should the bot scan the subreddit for these posts, in seconds
# Default is False to avoid accidental locking; set to True to enable locking. interval = 30
lock_post = False
# Whether the distinguished comment should be stickied (True/False) # Max posts to search (for performance)
# Some subreddits may require `True` to keep moderator comments visible. searchlimit = 600
distinguish_sticky = False
# Multiple flair time configs
# Each entry can have: flair_text, hours, comment_message, lock_post, distinguish_sticky
flair_times = [
{
"flair_text": "Waiting for OP", # Case Sensitive
"hours": 48, # How many hours must the flair been on the post to send the notification
"comment_message": "This post has had the 'Waiting for OP' flair for 48 hours.",
"lock_post": False,
"distinguish_sticky": False
},
# Add more configs as needed
# {
# "flair_text": "Needs Info",
# "hours": 24,
# "comment_message": "This post has had the 'Needs Info' flair for 24 hours.",
# "lock_post": True,
# "distinguish_sticky": True
# },
]
+22
View File
@@ -0,0 +1,22 @@
# flairconfig.py
# This file defines the list of flair time configs for the bot.
# Edit this file to customize flair behaviors.
flair_times = [
{
"flair_text": "Waiting for OP",
"hours": 48,
"comment_message": "This post has had the 'Waiting for OP' flair for 48 hours.",
"lock_post": False,
"distinguish_sticky": False
},
{
"flair_text": "Solved",
"hours": 0.01,
"comment_message": "This post has had the 'Waiting for OP' flair for 48 hours.",
"lock_post": False,
"distinguish_sticky": False
},
# Add more configs as needed
]
+2
View File
@@ -3,6 +3,8 @@ services:
image: ghcr.io/slfhstd/flairtimercomment:v0.0.1 image: ghcr.io/slfhstd/flairtimercomment:v0.0.1
container_name: bot-ftc-dev container_name: bot-ftc-dev
restart: unless-stopped restart: unless-stopped
env_file:
- .env
volumes: volumes:
- /docker/data/flairtimercomment:/app/config - /docker/data/flairtimercomment:/app/config
+18
View File
@@ -0,0 +1,18 @@
# Example .env file for FlairTimerCommentBot
# Fill in your Reddit API and bot settings
USERNAME=your_reddit_username
PASSWORD=your_reddit_password
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
USER_AGENT=Flair Timer Comment Bot
# Subreddit to monitor
SUBREDDIT=your_subreddit
INTERVAL=30
SEARCHLIMIT=600
# Multiple flair configs as JSON string
# Example:
# FLAIR_TIMES_JSON=[{"flair_text": "Waiting for OP", "hours": 48, "comment_message": "Waiting for OP for 48 hours.", "lock_post": false, "distinguish_sticky": false}, {"flair_text": "Needs Info", "hours": 24, "comment_message": "Needs Info for 24 hours.", "lock_post": true, "distinguish_sticky": true}]
FLAIR_TIMES_JSON=[{"flair_text": "Waiting for OP", "hours": 48, "comment_message": "Waiting for OP for 48 hours.", "lock_post": false, "distinguish_sticky": false}]
+132 -55
View File
@@ -1,98 +1,175 @@
import praw
import praw import praw
import os import os
import os.path import os.path
import json import json
import time import time
# Create default config/config.py if it doesn't exist
# Helper to get env var or default
def env_or_default(var, default):
return os.environ.get(var, default)
# Create config/config.py from environment if missing or empty
default_config_path = os.path.join('config', 'config.py') default_config_path = os.path.join('config', 'config.py')
if not os.path.exists(default_config_path):
def write_config_from_env():
os.makedirs('config', exist_ok=True) os.makedirs('config', exist_ok=True)
with open(default_config_path, 'w') as f: with open(default_config_path, 'w') as f:
f.write( f.write(
'username = ""\n' f'username = "{env_or_default("USERNAME", "")}"\n'
'password = ""\n' f'password = "{env_or_default("PASSWORD", "")}"\n'
'client_id = ""\n' f'client_id = "{env_or_default("CLIENT_ID", "")}"\n'
'client_secret = ""\n' f'client_secret = "{env_or_default("CLIENT_SECRET", "")}"\n'
'user_agent = "Flair Timer Comment Bot"\n' f'user_agent = "{env_or_default("USER_AGENT", "Flair Timer Comment Bot")}"\n'
'\n' '\n'
'# Subreddits\n' f'subreddit = "{env_or_default("SUBREDDIT", "")}"\n'
'subreddit = ""\n' f'interval = {env_or_default("INTERVAL", "30")}\n'
'flair_text = "Waiting for OP"\n' f'searchlimit = {env_or_default("SEARCHLIMIT", "600")}\n'
'interval = 30\n'
'hours = 48\n'
'searchlimit = 600\n'
'comment_message = ""\n'
'lock_post = False\n'
'distinguish_sticky = False\n'
) )
print(f"Configuration file auto-populated from environment variables at {default_config_path}.")
# Check if config file exists and is non-empty, else generate from env
def config_needs_populating():
if not os.path.exists(default_config_path):
return True
try:
with open(default_config_path, 'r') as f:
content = f.read().strip()
return len(content) == 0
except Exception:
return True
if config_needs_populating():
write_config_from_env()
# Import main config
import config import config
# Create default flairconfig.py if missing
flair_config_path = os.path.join('config', 'flairconfig.py')
def write_default_flairconfig():
if not os.path.exists(flair_config_path):
os.makedirs(os.path.dirname(flair_config_path), exist_ok=True)
with open(flair_config_path, 'w') as f:
f.write('# flairconfig.py\n')
f.write('# This file defines the list of flair time configs for the bot.\n')
f.write('flair_times = [\n')
f.write(' {\n')
f.write(' "flair_text": "Waiting for OP",\n')
f.write(' "hours": 48,\n')
f.write(' "comment_message": "This post has had the \'Waiting for OP\' flair for 48 hours.",\n')
f.write(' "lock_post": False,\n')
f.write(' "distinguish_sticky": False\n')
f.write(' },\n')
f.write(']\n')
print(f"Default flairconfig.py created at {flair_config_path}.")
write_default_flairconfig()
# Load flair_times from flairconfig.py
import importlib.util
spec = importlib.util.spec_from_file_location("flairconfig", flair_config_path)
flairconfig = importlib.util.module_from_spec(spec)
spec.loader.exec_module(flairconfig)
flair_times = getattr(flairconfig, "flair_times", [])
def authentication(): def authentication():
print ("Authenticating...") print("Authenticating...")
reddit = praw.Reddit(username = config.username, reddit = praw.Reddit(
password = config.password, username=config.username,
client_id = config.client_id, password=config.password,
client_secret = config.client_secret, client_id=config.client_id,
user_agent = config.user_agent) client_secret=config.client_secret,
print ("Authenticated as {}.".format(reddit.user.me())) user_agent=config.user_agent
)
print("Authenticated as {}.".format(reddit.user.me()))
return reddit return reddit
def main(reddit, posts: dict):
def main(reddit, all_posts: dict):
# all_posts structure: {flair_text: {submission_id: timestamp}}
while True: while True:
for submission in reddit.subreddit(config.subreddit).new(limit=config.searchlimit): for flair_cfg in flair_times:
if not submission.saved: flair_text = flair_cfg["flair_text"]
if submission.id not in posts.keys() and submission.link_flair_text == config.flair_text: hours = flair_cfg["hours"]
posts[submission.id] = time.time() comment_message = flair_cfg["comment_message"]
print(f"Post {submission} has been flaired {config.flair_text}") lock_post = flair_cfg.get("lock_post", False)
if submission.id in posts.keys() and submission.link_flair_text != config.flair_text: distinguish_sticky = flair_cfg.get("distinguish_sticky", False)
posts.pop(submission.id)
print(f"Post {submission} has been unflaired {config.flair_text}") # Ensure posts dict for this flair
posts = all_posts.setdefault(flair_text, {})
for submission in posts:
if time.time() > posts[submission] + (config.hours * 60 * 60): for submission in reddit.subreddit(config.subreddit).new(limit=config.searchlimit):
posts.pop(submission) if not submission.saved:
reddit.submission(submission).save() if submission.id not in posts and submission.link_flair_text == flair_text:
# Optionally lock the post if configured posts[submission.id] = time.time()
if getattr(config, 'lock_post', False): print(f"Post {submission} has been flaired {flair_text}")
if submission.id in posts and submission.link_flair_text != flair_text:
posts.pop(submission.id)
print(f"Post {submission} has been unflaired {flair_text}")
expired = []
for submission_id, flair_time in posts.items():
if time.time() > flair_time + (hours * 60 * 60):
expired.append(submission_id)
for submission_id in expired:
posts.pop(submission_id)
subm = reddit.submission(submission_id)
subm.save()
if lock_post:
try: try:
reddit.submission(submission).mod.lock() subm.mod.lock()
except Exception as e: except Exception as e:
print(f"Could not lock submission: {e}") print(f"Could not lock submission: {e}")
comment = reddit.submission(submission).reply(body=config.comment_message) comment = subm.reply(body=comment_message)
try: try:
sticky = getattr(config, 'distinguish_sticky', False) if distinguish_sticky:
if sticky:
comment.mod.distinguish(how="yes", sticky=True) comment.mod.distinguish(how="yes", sticky=True)
else: else:
comment.mod.distinguish(how="yes") comment.mod.distinguish(how="yes")
print(f"Distinguished comment (sticky={sticky})") print(f"Distinguished comment (sticky={distinguish_sticky})")
except Exception as e: except Exception as e:
print(f"Could not distinguish comment: {e}") print(f"Could not distinguish comment: {e}")
print(f"Post {submission} has been flaired {config.flair_text} for {config.hours} hours, posted comment") print(f"Post {submission_id} has been flaired {flair_text} for {hours} hours, posted comment")
break
save_posts(all_posts)
save_posts(posts)
time.sleep(config.interval) time.sleep(config.interval)
def load_posts(): def load_posts():
if not os.path.exists("config/posts.json"): if not os.path.exists("config/posts.json"):
with open("config/posts.json", "w+") as file: with open("config/posts.json", "w+") as file:
json.dump({}, file) json.dump({}, file)
with open("config/posts.json", "r+") as file: with open("config/posts.json", "r+") as file:
data = json.load(file) data = json.load(file)
# Ensure structure: {flair_text: {submission_id: timestamp}}
if not isinstance(data, dict):
return {}
return data return data
def save_posts(data): def save_posts(data):
with open('config/posts.json', 'w+') as file: with open('config/posts.json', 'w+') as file:
json.dump(data, file) json.dump(data, file)
while True: while True:
try: try:
posts = load_posts() posts = load_posts()
main(reddit = authentication(), posts = posts) main(reddit=authentication(), all_posts=posts)
except Exception as e: except Exception as e:
print(e) print(e)