a digital person for bluesky
1#!/usr/bin/env python3
2"""
3Configuration Migration Script for Void Bot
4Migrates from .env environment variables to config.yaml YAML configuration.
5"""
6
7import os
8import shutil
9from pathlib import Path
10import yaml
11from datetime import datetime
12
13
14def load_env_file(env_path=".env"):
15 """Load environment variables from .env file."""
16 env_vars = {}
17 if not os.path.exists(env_path):
18 return env_vars
19
20 try:
21 with open(env_path, 'r', encoding='utf-8') as f:
22 for line_num, line in enumerate(f, 1):
23 line = line.strip()
24 # Skip empty lines and comments
25 if not line or line.startswith('#'):
26 continue
27
28 # Parse KEY=VALUE format
29 if '=' in line:
30 key, value = line.split('=', 1)
31 key = key.strip()
32 value = value.strip()
33
34 # Remove quotes if present
35 if value.startswith('"') and value.endswith('"'):
36 value = value[1:-1]
37 elif value.startswith("'") and value.endswith("'"):
38 value = value[1:-1]
39
40 env_vars[key] = value
41 else:
42 print(f"⚠️ Warning: Skipping malformed line {line_num} in .env: {line}")
43 except Exception as e:
44 print(f"❌ Error reading .env file: {e}")
45
46 return env_vars
47
48
49def create_config_from_env(env_vars, existing_config=None):
50 """Create YAML configuration from environment variables."""
51
52 # Start with existing config if available, otherwise use defaults
53 if existing_config:
54 config = existing_config.copy()
55 else:
56 config = {}
57
58 # Ensure all sections exist
59 if 'letta' not in config:
60 config['letta'] = {}
61 if 'bluesky' not in config:
62 config['bluesky'] = {}
63 if 'bot' not in config:
64 config['bot'] = {}
65
66 # Map environment variables to config structure
67 env_mapping = {
68 'LETTA_API_KEY': ('letta', 'api_key'),
69 'BSKY_USERNAME': ('bluesky', 'username'),
70 'BSKY_PASSWORD': ('bluesky', 'password'),
71 'PDS_URI': ('bluesky', 'pds_uri'),
72 }
73
74 migrated_vars = []
75
76 for env_var, (section, key) in env_mapping.items():
77 if env_var in env_vars:
78 config[section][key] = env_vars[env_var]
79 migrated_vars.append(env_var)
80
81 # Set some sensible defaults if not already present
82 if 'timeout' not in config['letta']:
83 config['letta']['timeout'] = 600
84
85 if 'pds_uri' not in config['bluesky']:
86 config['bluesky']['pds_uri'] = "https://bsky.social"
87
88 # Add bot configuration defaults if not present
89 if 'fetch_notifications_delay' not in config['bot']:
90 config['bot']['fetch_notifications_delay'] = 30
91 if 'max_processed_notifications' not in config['bot']:
92 config['bot']['max_processed_notifications'] = 10000
93 if 'max_notification_pages' not in config['bot']:
94 config['bot']['max_notification_pages'] = 20
95
96 return config, migrated_vars
97
98
99def backup_existing_files():
100 """Create backups of existing configuration files."""
101 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
102 backups = []
103
104 # Backup existing config.yaml if it exists
105 if os.path.exists("config.yaml"):
106 backup_path = f"config.yaml.backup_{timestamp}"
107 shutil.copy2("config.yaml", backup_path)
108 backups.append(("config.yaml", backup_path))
109
110 # Backup .env if it exists
111 if os.path.exists(".env"):
112 backup_path = f".env.backup_{timestamp}"
113 shutil.copy2(".env", backup_path)
114 backups.append((".env", backup_path))
115
116 return backups
117
118
119def load_existing_config():
120 """Load existing config.yaml if it exists."""
121 if not os.path.exists("config.yaml"):
122 return None
123
124 try:
125 with open("config.yaml", 'r', encoding='utf-8') as f:
126 return yaml.safe_load(f) or {}
127 except Exception as e:
128 print(f"⚠️ Warning: Could not read existing config.yaml: {e}")
129 return None
130
131
132def write_config_yaml(config):
133 """Write the configuration to config.yaml."""
134 try:
135 with open("config.yaml", 'w', encoding='utf-8') as f:
136 # Write header comment
137 f.write("# Void Bot Configuration\n")
138 f.write("# Generated by migration script\n")
139 f.write(f"# Created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
140 f.write("# See config.yaml.example for all available options\n\n")
141
142 # Write YAML content
143 yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2)
144
145 return True
146 except Exception as e:
147 print(f"❌ Error writing config.yaml: {e}")
148 return False
149
150
151def main():
152 """Main migration function."""
153 print("🔄 Void Bot Configuration Migration Tool")
154 print("=" * 50)
155 print("This tool migrates from .env environment variables to config.yaml")
156 print()
157
158 # Check what files exist
159 has_env = os.path.exists(".env")
160 has_config = os.path.exists("config.yaml")
161 has_example = os.path.exists("config.yaml.example")
162
163 print("📋 Current configuration files:")
164 print(f" - .env file: {'✅ Found' if has_env else '❌ Not found'}")
165 print(f" - config.yaml: {'✅ Found' if has_config else '❌ Not found'}")
166 print(f" - config.yaml.example: {'✅ Found' if has_example else '❌ Not found'}")
167 print()
168
169 # If no .env file, suggest creating config from example
170 if not has_env:
171 if not has_config and has_example:
172 print("💡 No .env file found. Would you like to create config.yaml from the example?")
173 response = input("Create config.yaml from example? (y/n): ").lower().strip()
174 if response in ['y', 'yes']:
175 try:
176 shutil.copy2("config.yaml.example", "config.yaml")
177 print("✅ Created config.yaml from config.yaml.example")
178 print("📝 Please edit config.yaml to add your credentials")
179 return
180 except Exception as e:
181 print(f"❌ Error copying example file: {e}")
182 return
183 else:
184 print("👋 Migration cancelled")
185 return
186 else:
187 print("ℹ️ No .env file found and config.yaml already exists or no example available")
188 print(" If you need to set up configuration, see CONFIG.md")
189 return
190
191 # Load environment variables from .env
192 print("🔍 Reading .env file...")
193 env_vars = load_env_file()
194
195 if not env_vars:
196 print("⚠️ No environment variables found in .env file")
197 return
198
199 print(f" Found {len(env_vars)} environment variables")
200 for key in env_vars.keys():
201 # Mask sensitive values
202 if 'KEY' in key or 'PASSWORD' in key:
203 value_display = f"***{env_vars[key][-4:]}" if len(env_vars[key]) > 4 else "***"
204 else:
205 value_display = env_vars[key]
206 print(f" - {key}={value_display}")
207 print()
208
209 # Load existing config if present
210 existing_config = load_existing_config()
211 if existing_config:
212 print("📄 Found existing config.yaml - will merge with .env values")
213
214 # Create configuration
215 print("🏗️ Building configuration...")
216 config, migrated_vars = create_config_from_env(env_vars, existing_config)
217
218 if not migrated_vars:
219 print("⚠️ No recognized configuration variables found in .env")
220 print(" Recognized variables: LETTA_API_KEY, BSKY_USERNAME, BSKY_PASSWORD, PDS_URI")
221 return
222
223 print(f" Migrating {len(migrated_vars)} variables: {', '.join(migrated_vars)}")
224
225 # Show preview
226 print("\n📋 Configuration preview:")
227 print("-" * 30)
228
229 # Show Letta section
230 if 'letta' in config and config['letta']:
231 print("🔧 Letta:")
232 for key, value in config['letta'].items():
233 if 'key' in key.lower():
234 display_value = f"***{value[-8:]}" if len(str(value)) > 8 else "***"
235 else:
236 display_value = value
237 print(f" {key}: {display_value}")
238
239 # Show Bluesky section
240 if 'bluesky' in config and config['bluesky']:
241 print("🐦 Bluesky:")
242 for key, value in config['bluesky'].items():
243 if 'password' in key.lower():
244 display_value = f"***{value[-4:]}" if len(str(value)) > 4 else "***"
245 else:
246 display_value = value
247 print(f" {key}: {display_value}")
248
249 print()
250
251 # Confirm migration
252 response = input("💾 Proceed with migration? This will update config.yaml (y/n): ").lower().strip()
253 if response not in ['y', 'yes']:
254 print("👋 Migration cancelled")
255 return
256
257 # Create backups
258 print("💾 Creating backups...")
259 backups = backup_existing_files()
260 for original, backup in backups:
261 print(f" Backed up {original} → {backup}")
262
263 # Write new configuration
264 print("✍️ Writing config.yaml...")
265 if write_config_yaml(config):
266 print("✅ Successfully created config.yaml")
267
268 # Test the new configuration
269 print("\n🧪 Testing new configuration...")
270 try:
271 from config_loader import get_config
272 test_config = get_config()
273 print("✅ Configuration loads successfully")
274
275 # Test specific sections
276 try:
277 from config_loader import get_letta_config
278 letta_config = get_letta_config()
279 print("✅ Letta configuration valid")
280 except Exception as e:
281 print(f"⚠️ Letta config issue: {e}")
282
283 try:
284 from config_loader import get_bluesky_config
285 bluesky_config = get_bluesky_config()
286 print("✅ Bluesky configuration valid")
287 except Exception as e:
288 print(f"⚠️ Bluesky config issue: {e}")
289
290 except Exception as e:
291 print(f"❌ Configuration test failed: {e}")
292 return
293
294 # Success message and next steps
295 print("\n🎉 Migration completed successfully!")
296 print("\n📖 Next steps:")
297 print(" 1. Run: python test_config.py")
298 print(" 2. Test the bot: python bsky.py --test")
299 print(" 3. If everything works, you can optionally remove the .env file")
300 print(" 4. See CONFIG.md for more configuration options")
301
302 if backups:
303 print(f"\n🗂️ Backup files created:")
304 for original, backup in backups:
305 print(f" {backup}")
306 print(" These can be deleted once you verify everything works")
307
308 else:
309 print("❌ Failed to write config.yaml")
310 if backups:
311 print("🔄 Restoring backups...")
312 for original, backup in backups:
313 try:
314 if original != ".env": # Don't restore .env, keep it as fallback
315 shutil.move(backup, original)
316 print(f" Restored {backup} → {original}")
317 except Exception as e:
318 print(f" ❌ Failed to restore {backup}: {e}")
319
320
321if __name__ == "__main__":
322 main()