A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.

Feat: Add full configuration export/import functionality

jack c2ae4f73 268c2bcb

+119
+75
public/index.html
··· 394 394 } 395 395 }; 396 396 397 + const handleExportConfig = async () => { 398 + try { 399 + const response = await axios.get('/api/config/export', { 400 + headers: { Authorization: `Bearer ${token}` }, 401 + responseType: 'blob', 402 + }); 403 + const url = window.URL.createObjectURL(new Blob([response.data])); 404 + const link = document.createElement('a'); 405 + link.href = url; 406 + link.setAttribute('download', `tweets-2-bsky-config-${new Date().toISOString().split('T')[0]}.json`); 407 + document.body.appendChild(link); 408 + link.click(); 409 + link.remove(); 410 + } catch (err) { 411 + alert('Failed to export configuration'); 412 + } 413 + }; 414 + 415 + const handleImportConfig = async (e) => { 416 + const file = e.target.files[0]; 417 + if (!file) return; 418 + 419 + if (!confirm('This will OVERWRITE your current accounts and settings (except login). Are you sure?')) { 420 + e.target.value = ''; // Reset input 421 + return; 422 + } 423 + 424 + const reader = new FileReader(); 425 + reader.onload = async (event) => { 426 + try { 427 + const config = JSON.parse(event.target.result); 428 + await axios.post('/api/config/import', config, { 429 + headers: { Authorization: `Bearer ${token}` } 430 + }); 431 + alert('Configuration imported successfully! Reloading...'); 432 + fetchData(); 433 + } catch (err) { 434 + alert('Failed to import configuration: ' + (err.response?.data?.error || err.message)); 435 + } 436 + e.target.value = ''; // Reset input 437 + }; 438 + reader.readAsText(file); 439 + }; 440 + 397 441 if (view === 'login' || view === 'register' || !token) { 398 442 return ( 399 443 <div className="container d-flex justify-content-center align-items-center min-vh-100"> ··· 783 827 </div> 784 828 </div> 785 829 </div> 830 + </div> 831 + </div> 832 + </div> 833 + 834 + {/* Data Management */} 835 + <div className="card mt-4"> 836 + <div className="card-header"> 837 + <span className="d-flex align-items-center gap-2"> 838 + <span className="material-icons text-muted">save</span> Data Management 839 + </span> 840 + </div> 841 + <div className="card-body"> 842 + <div className="d-grid gap-2"> 843 + <button onClick={handleExportConfig} className="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-2"> 844 + <span className="material-icons" style={{fontSize: '18px'}}>download</span> Export Configuration 845 + </button> 846 + <div className="position-relative"> 847 + <input 848 + type="file" 849 + accept=".json" 850 + className="form-control d-none" 851 + id="importConfigInput" 852 + onChange={handleImportConfig} 853 + /> 854 + <button onClick={() => document.getElementById('importConfigInput').click()} className="btn btn-outline-primary btn-sm w-100 d-flex align-items-center justify-content-center gap-2"> 855 + <span className="material-icons" style={{fontSize: '18px'}}>upload</span> Import Configuration 856 + </button> 857 + </div> 858 + </div> 859 + <div className="form-text small mt-2 text-center"> 860 + Exports accounts, Twitter keys, and AI settings. User logins are NOT exported. 786 861 </div> 787 862 </div> 788 863 </div>
+44
src/server.ts
··· 301 301 res.json({ success: true, message: 'All backfills cleared' }); 302 302 }); 303 303 304 + // --- Config Management Routes --- 305 + 306 + app.get('/api/config/export', authenticateToken, requireAdmin, (_req, res) => { 307 + const config = getConfig(); 308 + // Create a copy without user data (passwords) 309 + const { users, ...cleanConfig } = config; 310 + 311 + res.setHeader('Content-Type', 'application/json'); 312 + res.setHeader('Content-Disposition', 'attachment; filename=tweets-2-bsky-config.json'); 313 + res.json(cleanConfig); 314 + }); 315 + 316 + app.post('/api/config/import', authenticateToken, requireAdmin, (req, res) => { 317 + try { 318 + const importData = req.body; 319 + const currentConfig = getConfig(); 320 + 321 + // Validate minimal structure 322 + if (!importData.mappings || !Array.isArray(importData.mappings)) { 323 + res.status(400).json({ error: 'Invalid config format: missing mappings array' }); 324 + return; 325 + } 326 + 327 + // Merge logic: 328 + // 1. Keep current users (don't overwrite admin/passwords) 329 + // 2. Overwrite mappings, twitter, ai config from import 330 + // 3. Keep current values if import is missing them (optional, but safer to just replace sections) 331 + 332 + const newConfig = { 333 + ...currentConfig, 334 + mappings: importData.mappings, 335 + twitter: importData.twitter || currentConfig.twitter, 336 + ai: importData.ai || currentConfig.ai, 337 + checkIntervalMinutes: importData.checkIntervalMinutes || currentConfig.checkIntervalMinutes 338 + }; 339 + 340 + saveConfig(newConfig); 341 + res.json({ success: true, message: 'Configuration imported successfully' }); 342 + } catch (err) { 343 + console.error('Import failed:', err); 344 + res.status(500).json({ error: 'Failed to process import file' }); 345 + } 346 + }); 347 + 304 348 app.get('/api/recent-activity', authenticateToken, (req, res) => { 305 349 const limit = req.query.limit ? Number(req.query.limit) : 50; 306 350 const tweets = dbService.getRecentProcessedTweets(limit);