Skip to content

Commit 999e752

Browse files
committed
feat: add environment variable support and action validation
- Add comprehensive environment variable support for all config options - Environment variables override config file, CLI args override both - Add VALID_MODLOG_ACTIONS constant with all known Reddit actions - Strict validation rejects invalid modlog actions with clear error messages - Support Docker/container deployment patterns - Updated documentation with env var examples and Docker usage
1 parent 74d93d5 commit 999e752

2 files changed

Lines changed: 163 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,76 @@ sqlite3 modlog.db "SELECT target_id, action_type, moderator, removal_reason, dat
5858
sqlite3 modlog.db "DELETE FROM processed_actions WHERE created_at < date('now', '-30 days');"
5959
```
6060

61-
## Key Configuration
61+
## Configuration
6262

63-
The application supports both JSON config files and CLI arguments (CLI overrides JSON):
63+
The application supports multiple configuration methods with the following priority (highest to lowest):
64+
1. **Command line arguments** (highest priority)
65+
2. **Environment variables** (override config file)
66+
3. **JSON config file** (base configuration)
6467

65-
### Core Options
68+
### Environment Variables
69+
70+
All configuration options can be set via environment variables:
71+
72+
#### Reddit Credentials
73+
- `REDDIT_CLIENT_ID`: Reddit app client ID
74+
- `REDDIT_CLIENT_SECRET`: Reddit app client secret
75+
- `REDDIT_USERNAME`: Reddit bot username
76+
- `REDDIT_PASSWORD`: Reddit bot password
77+
78+
#### Application Settings
79+
- `SOURCE_SUBREDDIT`: Target subreddit name
80+
- `WIKI_PAGE`: Wiki page name (default: "modlog")
81+
- `RETENTION_DAYS`: Database cleanup period in days
82+
- `BATCH_SIZE`: Entries fetched per run
83+
- `UPDATE_INTERVAL`: Seconds between updates in daemon mode
84+
- `ANONYMIZE_MODERATORS`: `true` or `false` for moderator anonymization
85+
86+
#### Advanced Settings
87+
- `WIKI_ACTIONS`: Comma-separated list of actions to show (e.g., "removelink,removecomment,approvelink")
88+
- `IGNORED_MODERATORS`: Comma-separated list of moderators to ignore
89+
90+
### Command Line Options
6691
- `--source-subreddit`: Target subreddit for reading/writing logs
6792
- `--wiki-page`: Wiki page name (default: "modlog")
6893
- `--retention-days`: Database cleanup period (default: 30)
6994
- `--batch-size`: Entries fetched per run (default: 100)
7095
- `--interval`: Seconds between updates in daemon mode (default: 300)
7196
- `--debug`: Enable verbose logging
7297

98+
### Configuration Examples
99+
100+
#### Using Environment Variables (Docker/Container)
101+
```bash
102+
# Set credentials via environment
103+
export REDDIT_CLIENT_ID="your_client_id"
104+
export REDDIT_CLIENT_SECRET="your_client_secret"
105+
export REDDIT_USERNAME="your_bot_username"
106+
export REDDIT_PASSWORD="your_bot_password"
107+
export SOURCE_SUBREDDIT="usenet"
108+
109+
# Run without config file
110+
python modlog_wiki_publisher.py
111+
```
112+
113+
#### Docker Example
114+
```bash
115+
docker run -e REDDIT_CLIENT_ID="id" \
116+
-e REDDIT_CLIENT_SECRET="secret" \
117+
-e REDDIT_USERNAME="bot" \
118+
-e REDDIT_PASSWORD="pass" \
119+
-e SOURCE_SUBREDDIT="usenet" \
120+
-e ANONYMIZE_MODERATORS="true" \
121+
your-modlog-image
122+
```
123+
124+
#### Mixed Configuration
125+
```bash
126+
# Use config file + env overrides + CLI args
127+
export SOURCE_SUBREDDIT="usenet" # Override config file
128+
python modlog_wiki_publisher.py --debug --batch-size 25 # CLI takes priority
129+
```
130+
73131
### Display Options
74132
- `anonymize_moderators`: Whether to show "HumanModerator" for human mods (default: true)
75133
- `true` (default): Shows "AutoModerator", "Reddit", or "HumanModerator"

modlog_wiki_publisher.py

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,32 @@
2929
REASON_ACTIONS = ['addremovalreason']
3030
DEFAULT_WIKI_ACTIONS = REMOVAL_ACTIONS + REASON_ACTIONS + APPROVAL_ACTIONS
3131

32+
# Valid Reddit modlog actions (hardcoded for validation)
33+
VALID_MODLOG_ACTIONS = [
34+
# Content removal
35+
'removelink', 'removecomment', 'spamlink', 'spamcomment',
36+
# Content approval
37+
'approvelink', 'approvecomment',
38+
# Moderation reasons and notes
39+
'addremovalreason', 'addnote', 'deletenote',
40+
# User actions
41+
'banuser', 'unbanuser', 'muteuser', 'unmuteuser', 'invitemoderator', 'acceptmoderatorinvite',
42+
# Post management
43+
'distinguish', 'undistinguish', 'sticky', 'unsticky', 'lock', 'unlock', 'marknsfw', 'unmarknsfw',
44+
# Wiki actions
45+
'wikirevise', 'wikipagelisted', 'wikipermlevel', 'wikibanned', 'wikicontributor',
46+
# Settings and rules
47+
'editsettings', 'editflair', 'createrule', 'editrule', 'deleterule', 'reorderrules',
48+
# Reports and modmail
49+
'ignorereports', 'unignorereports', 'request_assistance',
50+
# Community features
51+
'community_widgets', 'community_welcome_page', 'edit_post_requirements', 'edit_comment_requirements',
52+
# Saved responses
53+
'edit_saved_response',
54+
# Collections
55+
'addtocollection', 'removefromcollection'
56+
]
57+
3258
# Configuration limits and defaults
3359
CONFIG_LIMITS = {
3460
'retention_days': {'min': 1, 'max': 365, 'default': 90},
@@ -106,6 +132,23 @@ def validate_config_value(key, value, config_limits):
106132

107133
return value
108134

135+
def validate_wiki_actions(wiki_actions):
136+
"""Validate wiki_actions against known Reddit modlog actions"""
137+
if not isinstance(wiki_actions, list):
138+
raise ValueError("wiki_actions must be a list")
139+
140+
if not wiki_actions:
141+
logger.info("Empty wiki_actions, using defaults")
142+
return DEFAULT_WIKI_ACTIONS
143+
144+
invalid_actions = [action for action in wiki_actions if action not in VALID_MODLOG_ACTIONS]
145+
146+
if invalid_actions:
147+
raise ValueError(f"Invalid modlog actions: {invalid_actions}. Valid actions: {sorted(VALID_MODLOG_ACTIONS)}")
148+
149+
logger.info(f"Validated {len(wiki_actions)} wiki_actions: {wiki_actions}")
150+
return wiki_actions
151+
109152
def apply_config_defaults_and_limits(config):
110153
"""Apply default values and enforce limits on configuration"""
111154
for key, limits in CONFIG_LIMITS.items():
@@ -119,6 +162,8 @@ def apply_config_defaults_and_limits(config):
119162
if 'wiki_actions' not in config:
120163
config['wiki_actions'] = DEFAULT_WIKI_ACTIONS
121164
logger.info("Using default wiki_actions: removals, removal reasons, and approvals")
165+
else:
166+
config['wiki_actions'] = validate_wiki_actions(config['wiki_actions'])
122167

123168
# Validate required fields
124169
required_fields = ['reddit', 'source_subreddit']
@@ -1117,19 +1162,72 @@ def process_modlog_actions(reddit, config: Dict[str, Any]) -> List:
11171162
logger.error(f"Error processing modlog actions: {e}")
11181163
raise
11191164

1165+
def load_env_config() -> Dict[str, Any]:
1166+
"""Load configuration from environment variables"""
1167+
env_config = {}
1168+
1169+
# Reddit credentials
1170+
reddit_config = {}
1171+
if os.getenv('REDDIT_CLIENT_ID'):
1172+
reddit_config['client_id'] = os.getenv('REDDIT_CLIENT_ID')
1173+
if os.getenv('REDDIT_CLIENT_SECRET'):
1174+
reddit_config['client_secret'] = os.getenv('REDDIT_CLIENT_SECRET')
1175+
if os.getenv('REDDIT_USERNAME'):
1176+
reddit_config['username'] = os.getenv('REDDIT_USERNAME')
1177+
if os.getenv('REDDIT_PASSWORD'):
1178+
reddit_config['password'] = os.getenv('REDDIT_PASSWORD')
1179+
1180+
if reddit_config:
1181+
env_config['reddit'] = reddit_config
1182+
1183+
# Application settings
1184+
if os.getenv('SOURCE_SUBREDDIT'):
1185+
env_config['source_subreddit'] = os.getenv('SOURCE_SUBREDDIT')
1186+
if os.getenv('WIKI_PAGE'):
1187+
env_config['wiki_page'] = os.getenv('WIKI_PAGE')
1188+
if os.getenv('RETENTION_DAYS'):
1189+
env_config['retention_days'] = int(os.getenv('RETENTION_DAYS'))
1190+
if os.getenv('BATCH_SIZE'):
1191+
env_config['batch_size'] = int(os.getenv('BATCH_SIZE'))
1192+
if os.getenv('UPDATE_INTERVAL'):
1193+
env_config['update_interval'] = int(os.getenv('UPDATE_INTERVAL'))
1194+
if os.getenv('ANONYMIZE_MODERATORS'):
1195+
env_config['anonymize_moderators'] = os.getenv('ANONYMIZE_MODERATORS').lower() == 'true'
1196+
1197+
# Wiki actions (comma-separated list)
1198+
if os.getenv('WIKI_ACTIONS'):
1199+
try:
1200+
raw_actions = [action.strip() for action in os.getenv('WIKI_ACTIONS').split(',')]
1201+
env_config['wiki_actions'] = validate_wiki_actions(raw_actions)
1202+
except ValueError as e:
1203+
logger.error(f"WIKI_ACTIONS environment variable invalid: {e}")
1204+
raise
1205+
1206+
# Ignored moderators (comma-separated list)
1207+
if os.getenv('IGNORED_MODERATORS'):
1208+
env_config['ignored_moderators'] = [mod.strip() for mod in os.getenv('IGNORED_MODERATORS').split(',')]
1209+
1210+
return env_config
1211+
11201212
def load_config(config_path: str, auto_update: bool = True) -> Dict[str, Any]:
1121-
"""Load and validate configuration"""
1213+
"""Load and validate configuration from file and environment variables"""
11221214
try:
1123-
# Load existing config
1215+
# Load existing config from file
11241216
original_config = {}
11251217
config_updated = False
11261218

11271219
try:
11281220
with open(config_path, 'r') as f:
11291221
original_config = json.load(f)
11301222
except FileNotFoundError:
1131-
logger.error(f"Config file not found: {config_path}")
1132-
raise
1223+
logger.warning(f"Config file not found: {config_path}, using environment variables only")
1224+
original_config = {}
1225+
1226+
# Override with environment variables
1227+
env_config = load_env_config()
1228+
if env_config:
1229+
logger.info("Using environment variable overrides for configuration")
1230+
original_config.update(env_config)
11331231

11341232
# Store original config for comparison
11351233
config_before = original_config.copy()

0 commit comments

Comments
 (0)