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