document.addEventListener('DOMContentLoaded', () => { /* ---------------------------------------------------- * DOM Elements * -------------------------------------------------- */ const loginButton = document.getElementById('loginButton'); const logoutButton = document.getElementById('logoutButton'); const userInfo = document.getElementById('userInfo'); const userPubkey = document.getElementById('userPubkey'); const authSection = document.getElementById('auth-section'); const mainContent = document.getElementById('main-content'); const botsList = document.getElementById('bots-list'); const createBotBtn = document.getElementById('create-bot-btn'); const saveBotBtn = document.getElementById('save-bot-btn'); // Modal elements for new bot creation const createBotModalEl = document.getElementById('createBotModal'); // New form fields for key import/generation const keyOption = document.getElementById('keyOption'); const nsecKeyInput = document.getElementById('nsecKeyInput'); const toggleNsecKey = document.getElementById('toggleNsecKey'); // If you have a "bot settings" modal: // (IDs must match the modals in your HTML) const botSettingsModalEl = document.getElementById('botSettingsModal'); /* ---------------------------------------------------- * Bootstrap Modal instance * -------------------------------------------------- */ let createBotModal; if (typeof bootstrap !== 'undefined' && createBotModalEl) { createBotModal = new bootstrap.Modal(createBotModalEl); } // If you have a Bot Settings modal let botSettingsModal; if (typeof bootstrap !== 'undefined' && botSettingsModalEl) { botSettingsModal = new bootstrap.Modal(botSettingsModalEl); } /* ---------------------------------------------------- * Global State * -------------------------------------------------- */ let currentUser = null; const API_ENDPOINT = ''; // <--- If your server is at http://localhost:8765, then use that. Example: 'http://localhost:8765' /* ---------------------------------------------------- * On page load, check if already logged in * -------------------------------------------------- */ checkAuth(); /* ---------------------------------------------------- * Event Listeners * -------------------------------------------------- */ if (loginButton) loginButton.addEventListener('click', login); if (logoutButton) logoutButton.addEventListener('click', logout); if (createBotBtn) createBotBtn.addEventListener('click', showCreateBotModal); if (saveBotBtn) saveBotBtn.addEventListener('click', createBot); document.body.addEventListener('click', (evt) => { const viewBtn = evt.target.closest('.view-bot'); if (viewBtn) { const botId = viewBtn.dataset.id; if (botId) { openBotSettings(botId); } } }); // Handle "keyOption" dropdown changes if (keyOption) { keyOption.addEventListener('change', function () { if (this.value === 'import') { nsecKeyInput.style.display = 'block'; // Focus the NSEC input field (slight delay to ensure DOM is ready) setTimeout(() => { const nk = document.getElementById('botNsecKey'); if (nk) nk.focus(); }, 100); } else { nsecKeyInput.style.display = 'none'; } }); } // Toggle NSEC key visibility if (toggleNsecKey) { toggleNsecKey.addEventListener('click', function () { const nsecKeyField = document.getElementById('botNsecKey'); if (!nsecKeyField) return; if (nsecKeyField.type === 'password') { nsecKeyField.type = 'text'; this.innerHTML = ` `; } else { nsecKeyField.type = 'password'; this.innerHTML = ` `; } }); } /* ---------------------------------------------------- * Authentication / Login / Logout * -------------------------------------------------- */ async function checkAuth() { const token = localStorage.getItem('authToken'); if (!token) { showAuthSection(); return; } try { const response = await fetch(`${API_ENDPOINT}/api/auth/verify`, { headers: { 'Authorization': token } }); if (response.ok) { const data = await response.json(); // data.pubkey is currently hex // Convert to npub using nip19 from nostr-tools const { nip19 } = window.nostrTools; const userNpub = nip19.npubEncode(data.pubkey); // Store npub as currentUser currentUser = userNpub; showMainContent(); // Always load bots if on the main page fetchBots(); } else { // Token invalid localStorage.removeItem('authToken'); showAuthSection(); } } catch (error) { console.error('Auth check failed:', error); showAuthSection(); } } async function login() { if (!window.nostr) { alert('Nostr extension not found. Please install a NIP-07 compatible extension like nos2x or Alby.'); return; } try { // Get user's public key const pubkey = await window.nostr.getPublicKey(); // Create challenge event for signing const event = { kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['challenge', 'nostr-poster-auth']], content: 'Authenticate with Nostr Poster' }; // Sign the event const signedEvent = await window.nostr.signEvent(event); // Send to server for verification const response = await fetch(`${API_ENDPOINT}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pubkey: pubkey, signature: signedEvent.sig, event: JSON.stringify(signedEvent) }) }); if (response.ok) { const data = await response.json(); localStorage.setItem('authToken', data.token); currentUser = pubkey; showMainContent(); fetchBots(); } else { alert('Authentication failed'); } } catch (error) { console.error('Login failed:', error); alert('Login failed: ' + error.message); } } function logout() { localStorage.removeItem('authToken'); currentUser = null; showAuthSection(); } /* ---------------------------------------------------- * UI State Helpers * -------------------------------------------------- */ function showAuthSection() { if (authSection) authSection.classList.remove('d-none'); if (mainContent) mainContent.classList.add('d-none'); if (loginButton) loginButton.classList.remove('d-none'); if (userInfo) userInfo.classList.add('d-none'); if (logoutButton) logoutButton.classList.add('d-none'); } function showMainContent() { if (authSection) authSection.classList.add('d-none'); if (mainContent) mainContent.classList.remove('d-none'); if (loginButton) loginButton.classList.add('d-none'); if (userInfo) userInfo.classList.remove('d-none'); if (logoutButton) logoutButton.classList.remove('d-none'); // Truncate pubkey for display if (userPubkey && currentUser) { const shortPubkey = currentUser.substring(0, 8) + '...' + currentUser.substring(currentUser.length - 4); userPubkey.textContent = shortPubkey; } } /* ---------------------------------------------------- * Bots List * -------------------------------------------------- */ async function fetchBots() { // If there's no #bots-list, skip if (!botsList) return; try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots`, { headers: { 'Authorization': token } }); if (response.ok) { const bots = await response.json(); renderBots(bots); } else { console.error('Failed to fetch bots'); } } catch (error) { console.error('Error fetching bots:', error); } } function renderBots(bots) { if (!botsList) return; botsList.innerHTML = ''; if (!bots || bots.length === 0) { botsList.innerHTML = `

You don't have any bots yet.

`; return; } bots.forEach(bot => { let statusBadge = ''; if (bot.post_config?.enabled) { statusBadge = 'Active'; } else { statusBadge = 'Inactive'; } const profileColor = generateColorFromString(bot.pubkey); const initials = (bot.name || 'Bot').substring(0, 2).toUpperCase(); const card = document.createElement('div'); card.className = 'col-md-4 mb-4'; card.innerHTML = `
${initials}
${bot.name}
${bot.display_name || ''}
${statusBadge}

${bot.bio || 'No bio'}

npub...${bot.pubkey.substring(bot.pubkey.length - 8)}

`; botsList.appendChild(card); }); // Helper function to generate a color from a string function generateColorFromString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } // Use the hash to create a hue in the purple range (260-290) const hue = ((hash % 30) + 260) % 360; return `hsl(${hue}, 70%, 60%)`; } } window.publishBotProfile = async function(botId) { try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}/profile/publish`, { method: 'POST', headers: { 'Authorization': token } }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || 'Failed to publish profile'); } const data = await response.json(); // Check if event data with NIP-19 encoding is available if (data.event) { // Create a modal to show the encoded event info const modalId = 'eventInfoModal'; // Remove any existing modal with the same ID const existingModal = document.getElementById(modalId); if (existingModal) { existingModal.remove(); } const eventModal = document.createElement('div'); eventModal.className = 'modal fade'; eventModal.id = modalId; eventModal.setAttribute('tabindex', '-1'); eventModal.innerHTML = ` `; // Add the modal to the document document.body.appendChild(eventModal); // Show the modal using Bootstrap const bsModal = new bootstrap.Modal(document.getElementById(modalId)); bsModal.show(); } alert('Profile published successfully!'); } catch (err) { console.error('Error publishing profile:', err); alert(`Error publishing profile: ${err.message}`); } }; /* ---------------------------------------------------- * Show Create Bot Modal * -------------------------------------------------- */ function showCreateBotModal() { // Reset form const createBotForm = document.getElementById('create-bot-form'); if (createBotForm) createBotForm.reset(); // Default: keyOption = "generate" → hide nsecKeyInput if (keyOption) { keyOption.value = 'generate'; } if (nsecKeyInput) { nsecKeyInput.style.display = 'none'; } // Show modal if (createBotModal) { createBotModal.show(); } } /* ---------------------------------------------------- * Create Bot (merged logic) * -------------------------------------------------- */ async function createBot() { console.clear(); console.log('Creating new bot...'); // Get form values const name = document.getElementById('botName').value.trim(); const displayName = document.getElementById('botDisplayName').value.trim(); const bio = document.getElementById('botBio').value.trim(); const nip05 = document.getElementById('botNip05').value.trim(); const keyChoice = keyOption ? keyOption.value : 'generate'; // fallback // Validate form if (!name) { alert('Bot name is required.'); return; } // Build request data const requestData = { name: name, display_name: displayName, bio: bio, nip05: nip05 }; // If user selected "import", grab the NSEC key if (keyChoice === 'import') { const nsecKey = document.getElementById('botNsecKey').value.trim(); if (!nsecKey) { alert('NSEC key is required when importing an existing key.'); return; } requestData.encrypted_privkey = nsecKey; console.log('Using imported NSEC key (starts with):', nsecKey.substring(0, 4) + '...'); } else { console.log('Using auto-generated keypair'); } console.log('Sending request to create bot...'); try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': token }, body: JSON.stringify(requestData) }); console.log('Response status:', response.status); const rawResponse = await response.text(); console.log('Response text:', rawResponse.substring(0, 200) + '...'); let jsonResponse; try { jsonResponse = JSON.parse(rawResponse); } catch (e) { console.error('Failed to parse JSON response:', e); } if (response.ok) { console.log('Bot created successfully!'); // Hide modal if (typeof bootstrap !== 'undefined') { const modal = bootstrap.Modal.getInstance(createBotModalEl); if (modal) modal.hide(); } // Refresh bot list fetchBots(); } else { const errorMsg = jsonResponse && jsonResponse.error ? jsonResponse.error : `Failed with status ${response.status}`; alert('Failed to create bot: ' + errorMsg); } } catch (error) { console.error('Error creating bot:', error); alert('Error creating bot: ' + error.message); } } /* ---------------------------------------------------- * Enable Bot * -------------------------------------------------- */ window.enableBot = async function (botId) { try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}/enable`, { method: 'POST', headers: { 'Authorization': token } }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || 'Failed to enable bot'); } alert('Bot enabled successfully!'); // Reload bots fetchBots(); } catch (err) { console.error('Error enabling bot:', err); alert(`Error enabling bot: ${err.message}`); } }; /* ---------------------------------------------------- * Disable Bot * -------------------------------------------------- */ window.disableBot = async function (botId) { try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}/disable`, { method: 'POST', headers: { 'Authorization': token, } }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || 'Failed to disable bot'); } alert('Bot paused/stopped successfully!'); // Refresh the list so it shows "Inactive" fetchBots(); } catch (err) { console.error('Error disabling bot:', err); alert(`Error disabling bot: ${err.message}`); } }; /* ---------------------------------------------------- * Settings Window * -------------------------------------------------- */ window.openBotSettings = async function (botId) { try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}`, { headers: { 'Authorization': token } }); if (!response.ok) { throw new Error('Failed to load bot data'); } const bot = await response.json(); // Fill form fields document.getElementById('botSettingsName').value = bot.name || ''; document.getElementById('botSettingsDisplayName').value = bot.display_name || ''; document.getElementById('botSettingsBio').value = bot.bio || ''; document.getElementById('botSettingsNip05').value = bot.nip05 || ''; document.getElementById('botSettingsZap').value = bot.zap_address || ''; // If post_config is present if (bot.post_config) { document.getElementById('botSettingsInterval').value = bot.post_config.interval_minutes || 60; // hashtags is stored as a JSON string const hashtagsJson = bot.post_config.hashtags || '[]'; const tagsArr = JSON.parse(hashtagsJson); document.getElementById('botSettingsHashtags').value = tagsArr.join(', '); } // Show the modal const modalEl = document.getElementById('botSettingsModal'); const modal = bootstrap.Modal.getOrCreateInstance(modalEl); modal.show(); // Store bot ID so we know which bot to save document.getElementById('botSettingsSaveBtn').setAttribute('data-bot-id', botId); } catch (err) { console.error('Error loading bot data:', err); alert('Error loading bot: ' + err.message); } }; window.saveBotSettings = async function () { const botId = document.getElementById('botSettingsSaveBtn').getAttribute('data-bot-id'); if (!botId) { return alert('No bot ID found'); } // Basic info const name = document.getElementById('botSettingsName').value.trim(); const displayName = document.getElementById('botSettingsDisplayName').value.trim(); const bio = document.getElementById('botSettingsBio').value.trim(); const nip05 = document.getElementById('botSettingsNip05').value.trim(); const zap = document.getElementById('botSettingsZap').value.trim(); // 1) Update the basic fields (PUT /api/bots/:id) try { const token = localStorage.getItem('authToken'); const updateResp = await fetch(`${API_ENDPOINT}/api/bots/${botId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': token }, body: JSON.stringify({ name, display_name: displayName, bio, nip05, zap_address: zap }) }); if (!updateResp.ok) { const errData = await updateResp.json().catch(() => ({})); throw new Error(errData.error || 'Failed to update bot info'); } } catch (err) { console.error('Failed to update bot info:', err); alert('Error updating bot info: ' + err.message); return; } // 2) Update the post config (PUT /api/bots/:id/config) const intervalValue = parseInt(document.getElementById('botSettingsInterval').value, 10) || 60; const hashtagsStr = document.getElementById('botSettingsHashtags').value.trim(); const hashtagsArr = hashtagsStr.length ? hashtagsStr.split(',').map(s => s.trim()) : []; const configPayload = { post_config: { interval_minutes: intervalValue, hashtags: JSON.stringify(hashtagsArr) // We do not override 'enabled' here, so it remains as is } }; try { const token = localStorage.getItem('authToken'); const configResp = await fetch(`${API_ENDPOINT}/api/bots/${botId}/config`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': token }, body: JSON.stringify(configPayload) }); if (!configResp.ok) { const errData = await configResp.json().catch(() => ({})); throw new Error(errData.error || 'Failed to update post config'); } } catch (err) { console.error('Failed to update post config:', err); alert('Error updating post config: ' + err.message); return; } alert('Bot settings updated!'); // Hide modal const modalEl = document.getElementById('botSettingsModal'); const modal = bootstrap.Modal.getInstance(modalEl); if (modal) modal.hide(); // Reload bots fetchBots(); }; /* ---------------------------------------------------- * Nuke bot * -------------------------------------------------- */ window.deleteBot = async function (botId) { // Safety check: prompt user to confirm if (!confirm('Are you sure you want to delete this bot? This cannot be undone.')) { return; } try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}`, { method: 'DELETE', headers: { 'Authorization': token } }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || `Failed to delete bot (status: ${response.status})`); } alert('Bot deleted successfully!'); // Reload the bots so it disappears from the list fetchBots(); } catch (err) { console.error('Error deleting bot:', err); alert(`Error deleting bot: ${err.message}`); } }; /* ---------------------------------------------------- * Content managment - Exposed as global functions * -------------------------------------------------- */ // Called by content.html after we confirm the user is logged in window.loadBotChoices = async function() { try { const token = localStorage.getItem('authToken'); if (!token) return; const res = await fetch(`${API_ENDPOINT}/api/bots`, { headers: { 'Authorization': token } }); if (!res.ok) { throw new Error('Failed to fetch bots'); } const bots = await res.json(); const botSelect = document.getElementById('botSelect'); if (!botSelect) return; botSelect.innerHTML = ``; bots.forEach(bot => { botSelect.innerHTML += ``; }); } catch (err) { console.error('Error loading bot choices:', err); } }; // Called when user picks a bot from the dropdown and clicks "Load Content" window.loadBotContent = async function() { const botSelect = document.getElementById('botSelect'); if (!botSelect) return; const botId = botSelect.value; if (!botId) { alert('Please select a bot first!'); return; } try { const token = localStorage.getItem('authToken'); const res = await fetch(`${API_ENDPOINT}/api/content/${botId}`, { headers: { 'Authorization': token } }); if (!res.ok) { throw new Error('Failed to list content'); } const files = await res.json(); renderBotContent(botId, files); } catch (err) { console.error('Error loading content:', err); alert('Error loading content: ' + err.message); } }; function renderBotContent(botId, files) { const container = document.getElementById('contentArea'); if (!container) return; // Clear existing container.innerHTML = `

Content for Bot #${botId}

`; if (!files || files.length === 0) { container.innerHTML += `

No files.

`; return; } // Show a simple list let listHtml = ``; container.innerHTML += listHtml; } // Upload a file window.uploadBotFile = async function(botId) { const fileInput = document.getElementById('uploadFileInput'); if (!fileInput || !fileInput.files.length) { return alert('No file selected'); } const formData = new FormData(); formData.append('file', fileInput.files[0]); try { const token = localStorage.getItem('authToken'); const res = await fetch(`${API_ENDPOINT}/api/content/${botId}/upload`, { method: 'POST', headers: { 'Authorization': token }, body: formData }); if (!res.ok) { throw new Error('Upload failed'); } await res.json(); // read the body alert('File uploaded!'); window.loadBotContent(botId); // refresh the list } catch (err) { console.error('Error uploading file:', err); alert('Upload error: ' + err.message); } }; // Delete window.deleteBotFile = async function(botId, filename) { if (!confirm(`Delete file "${filename}"?`)) return; try { const token = localStorage.getItem('authToken'); const res = await fetch(`${API_ENDPOINT}/api/content/${botId}/${filename}`, { method: 'DELETE', headers: { 'Authorization': token } }); if (!res.ok) { throw new Error('Failed to delete file'); } alert('Deleted file'); window.loadBotContent(botId); // refresh } catch (err) { console.error('Error deleting file:', err); alert('Delete error: ' + err.message); } }; // If #logoutButton2 exists (used on content.html), attach event listener const logoutButton2 = document.getElementById('logoutButton2'); if (logoutButton2) { logoutButton2.addEventListener('click', logout); } // Expose checkAuth to global context for content.html window.checkAuth = checkAuth; });