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'); const profilePictureInput = document.getElementById('botProfilePicture'); const uploadProfilePictureBtn = document.getElementById('uploadProfilePicture'); const profilePicturePreview = document.getElementById('profilePicturePreview'); const profilePreviewImage = document.getElementById('profilePreviewImage'); const removeProfilePictureBtn = document.getElementById('removeProfilePicture'); const profilePictureUrl = document.getElementById('profilePictureUrl'); // Banner image upload handlers for Create Bot Modal const bannerInput = document.getElementById('botBanner'); const uploadBannerBtn = document.getElementById('uploadBanner'); const bannerPreview = document.getElementById('bannerPreview'); const bannerPreviewImage = document.getElementById('bannerPreviewImage'); const removeBannerBtn = document.getElementById('removeBanner'); const bannerUrl = document.getElementById('bannerUrl'); // Profile image upload handlers for Bot Settings Modal const settingsProfilePictureInput = document.getElementById('botSettingsProfilePicture'); const uploadSettingsProfilePictureBtn = document.getElementById('uploadSettingsProfilePicture'); const settingsProfilePicturePreview = document.getElementById('settingsProfilePicturePreview'); const settingsProfilePictureContainer = document.getElementById('settingsProfilePictureContainer'); const settingsProfilePictureEmpty = document.getElementById('settingsProfilePictureEmpty'); const settingsProfilePreviewImage = document.getElementById('settingsProfilePreviewImage'); const removeSettingsProfilePictureBtn = document.getElementById('removeSettingsProfilePicture'); const settingsProfilePictureUrl = document.getElementById('settingsProfilePictureUrl'); // Banner image upload handlers for Bot Settings Modal const settingsBannerInput = document.getElementById('botSettingsBanner'); const uploadSettingsBannerBtn = document.getElementById('uploadSettingsBanner'); const settingsBannerPreview = document.getElementById('settingsBannerPreview'); const settingsBannerContainer = document.getElementById('settingsBannerContainer'); const settingsBannerEmpty = document.getElementById('settingsBannerEmpty'); const settingsBannerPreviewImage = document.getElementById('settingsBannerPreviewImage'); const removeSettingsBannerBtn = document.getElementById('removeSettingsBanner'); const settingsBannerUrl = document.getElementById('settingsBannerUrl'); const globalRelaysList = document.getElementById('global-relays-list'); const addGlobalRelayBtn = document.getElementById('add-global-relay-btn'); const saveGlobalRelayBtn = document.getElementById('save-global-relay-btn'); const globalRelayModalEl = document.getElementById('globalRelayModal'); /* ---------------------------------------------------- * 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); } let profileImageURL = ''; let bannerImageURL = ''; let settingsProfileImageURL = ''; let settingsBannerImageURL = ''; let globalRelays = []; let globalRelayModal; /* ---------------------------------------------------- * Global State * -------------------------------------------------- */ let currentUser = null; // Define API_ENDPOINT using relative URLs to fix CORS issues when running behind a proxy const API_ENDPOINT = ''; /* ---------------------------------------------------- * On page load, check if already logged in * -------------------------------------------------- */ checkAuth(); /* ---------------------------------------------------- * Event Listeners * -------------------------------------------------- */ if (loginButton) loginButton.addEventListener('click', login); if (logoutButton) logoutButton.addEventListener('click', logout); if (addGlobalRelayBtn) addGlobalRelayBtn.addEventListener('click', showAddGlobalRelayModal); if (saveGlobalRelayBtn) saveGlobalRelayBtn.addEventListener('click', addGlobalRelay); 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 = ` `; } }); } if (uploadProfilePictureBtn) { uploadProfilePictureBtn.addEventListener('click', () => handleImageUpload('profile', profilePictureInput)); } if (uploadBannerBtn) { uploadBannerBtn.addEventListener('click', () => handleImageUpload('banner', bannerInput)); } if (removeProfilePictureBtn) { removeProfilePictureBtn.addEventListener('click', () => { profilePicturePreview.classList.add('d-none'); profileImageURL = ''; }); } if (removeBannerBtn) { removeBannerBtn.addEventListener('click', () => { bannerPreview.classList.add('d-none'); bannerImageURL = ''; }); } if (uploadSettingsProfilePictureBtn) { uploadSettingsProfilePictureBtn.addEventListener('click', () => handleImageUpload('settings-profile', settingsProfilePictureInput)); } if (uploadSettingsBannerBtn) { uploadSettingsBannerBtn.addEventListener('click', () => handleImageUpload('settings-banner', settingsBannerInput)); } if (removeSettingsProfilePictureBtn) { removeSettingsProfilePictureBtn.addEventListener('click', () => { settingsProfilePictureContainer.classList.add('d-none'); settingsProfilePictureEmpty.classList.remove('d-none'); settingsProfileImageURL = ''; }); } if (removeSettingsBannerBtn) { removeSettingsBannerBtn.addEventListener('click', () => { settingsBannerContainer.classList.add('d-none'); settingsBannerEmpty.classList.remove('d-none'); settingsBannerImageURL = ''; }); } /* ---------------------------------------------------- * Authentication / Login / Logout * -------------------------------------------------- */ // In main.js, look at the checkAuth function 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(); // Convert to npub using nip19 from nostr-tools const { nip19 } = window.nostrTools; const userNpub = nip19.npubEncode(data.pubkey); currentUser = userNpub; showMainContent(); fetchBots(); fetchGlobalRelays(); // Make sure this function is called } else { 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); } } 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(); const { nip19 } = window.nostrTools; const userNpub = nip19.npubEncode(data.pubkey); currentUser = userNpub; showMainContent(); fetchBots(); fetchGlobalRelays(); // Add this line to load global relays } else { localStorage.removeItem('authToken'); showAuthSection(); } } catch (error) { console.error('Auth check failed:', error); showAuthSection(); } } 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) { // Find the publish button const publishBtn = document.getElementById('publishProfileBtn'); // If not found, try to find it by class (might be in bot card) const btnSelector = publishBtn || document.querySelector(`button[onclick*="publishBotProfile(${botId})"]`); // Create a loading state for the button if found let originalBtnText = ''; if (btnSelector) { originalBtnText = btnSelector.innerHTML; btnSelector.disabled = true; btnSelector.innerHTML = ' Publishing...'; } 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) { // Display success with NIP-19 encoded identifiers displaySuccessfulPublish(data.event); } else { // Basic success message if NIP-19 data isn't available alert('Profile published successfully!'); } } catch (err) { console.error('Error publishing profile:', err); alert(`Error publishing profile: ${err.message}`); } finally { // Reset button state if found if (btnSelector) { btnSelector.disabled = false; btnSelector.innerHTML = originalBtnText; } } }; // Helper function to display a success message with NIP-19 encoded event IDs function displaySuccessfulPublish(eventData) { // Create a modal to show the encoded event info const modalId = 'profilePublishedModal'; // Remove any existing modal with the same ID const existingModal = document.getElementById(modalId); if (existingModal) { existingModal.remove(); } // Create the modal const modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = modalId; modal.setAttribute('tabindex', '-1'); modal.setAttribute('aria-labelledby', `${modalId}Label`); modal.setAttribute('aria-hidden', 'true'); // Create the modal content modal.innerHTML = ` `; // Add the modal to the document document.body.appendChild(modal); // Show the modal const modalInstance = new bootstrap.Modal(document.getElementById(modalId)); modalInstance.show(); // Add event listeners to copy buttons document.querySelectorAll('.copy-btn').forEach(button => { button.addEventListener('click', function() { const text = this.getAttribute('data-value'); navigator.clipboard.writeText(text).then(() => { // Visual feedback that copy succeeded const originalHTML = this.innerHTML; this.innerHTML = ` `; setTimeout(() => { this.innerHTML = originalHTML; }, 2000); }); }); }); } /* ---------------------------------------------------- * Show Create Bot Modal * -------------------------------------------------- */ function showCreateBotModal() { // Reset form const createBotForm = document.getElementById('create-bot-form'); if (createBotForm) createBotForm.reset(); // Default: keyOption = "generate" → hide nsecKeyInput const keyOption = document.getElementById('keyOption'); const nsecKeyInput = document.getElementById('nsecKeyInput'); if (keyOption) { keyOption.value = 'generate'; } if (nsecKeyInput) { nsecKeyInput.style.display = 'none'; } // Show modal - ensure modal is initialized if (typeof bootstrap !== 'undefined' && createBotModalEl) { if (!createBotModal) { createBotModal = new bootstrap.Modal(createBotModalEl); } createBotModal.show(); } else { console.error('Bootstrap or modal element not found'); } } async function fetchGlobalRelays() { if (!globalRelaysList) return; try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/global-relays`, { headers: { 'Authorization': token } }); if (response.ok) { const relays = await response.json(); renderGlobalRelays(relays); } else { console.error('Failed to fetch global relays'); globalRelaysList.innerHTML = '
Failed to load global relays
'; } } catch (error) { console.error('Error fetching global relays:', error); globalRelaysList.innerHTML = '
Error loading global relays
'; } } // Render global relays function renderGlobalRelays(relays) { if (!globalRelaysList) return; globalRelaysList.innerHTML = ''; if (!relays || relays.length === 0) { globalRelaysList.innerHTML = `
No global relays configured. Add a relay to get started.
`; return; } let html = '
'; relays.forEach(relay => { html += `
${relay.url}
Read: ${relay.read ? 'Yes' : 'No'} Write: ${relay.write ? 'Yes' : 'No'}
`; }); html += '
'; globalRelaysList.innerHTML = html; } // Add this function for global relay modal function showAddGlobalRelayModal() { // Reset form const globalRelayForm = document.getElementById('global-relay-form'); if (globalRelayForm) globalRelayForm.reset(); // Show modal if (typeof bootstrap !== 'undefined' && globalRelayModalEl) { if (!globalRelayModal) { globalRelayModal = new bootstrap.Modal(globalRelayModalEl); } globalRelayModal.show(); } else { console.error('Bootstrap or modal element not found'); } } document.addEventListener('click', function(e) { if (typeof bootstrap !== 'undefined' && globalRelayModalEl) { globalRelayModal = new bootstrap.Modal(globalRelayModalEl); } if (e.target.closest('.copy-btn')) { const btn = e.target.closest('.copy-btn'); const valueToCopy = btn.getAttribute('data-value'); navigator.clipboard.writeText(valueToCopy) .then(() => { const originalHTML = btn.innerHTML; btn.innerHTML = 'Copied!'; setTimeout(() => { btn.innerHTML = originalHTML; }, 2000); }) .catch(err => { console.error('Failed to copy: ', err); }); } }); /* ---------------------------------------------------- * Image Stuff * -------------------------------------------------- */ async function handleImageUpload(type, fileInput) { if (!fileInput || !fileInput.files.length) { alert('Please select a file first'); return; } const file = fileInput.files[0]; // Validate file is an image if (!file.type.startsWith('image/')) { alert('Please select an image file'); return; } // Get the current bot ID if we're in settings mode let botId = null; if (type.startsWith('settings')) { const botSettingsSaveBtn = document.getElementById('botSettingsSaveBtn'); if (botSettingsSaveBtn) { botId = botSettingsSaveBtn.getAttribute('data-bot-id'); if (!botId) { alert('Could not determine bot ID'); return; } } else { alert('Could not find save button'); return; } } try { // Show loading state const uploadButtonId = type === 'profile' ? 'uploadProfilePicture' : type === 'banner' ? 'uploadBanner' : type === 'settings-profile' ? 'uploadSettingsProfilePicture' : 'uploadSettingsBanner'; const uploadButton = document.getElementById(uploadButtonId); const originalButtonText = uploadButton.innerHTML; uploadButton.disabled = true; uploadButton.innerHTML = ' Uploading...'; // For new bot creation (no botId yet), just preview the file if (!botId) { const reader = new FileReader(); reader.onload = function(e) { // For new bots, just show a preview and store the DataURL temporarily // The actual upload will happen when creating the bot previewUploadedImage(type, e.target.result, file.name); }; reader.readAsDataURL(file); // Reset upload button uploadButton.disabled = false; uploadButton.innerHTML = originalButtonText; return; } // Create form data for existing bots const formData = new FormData(); formData.append('file', file); // Upload file to server const token = localStorage.getItem('authToken'); // Upload through content endpoint const response = await fetch(`${API_ENDPOINT}/api/content/${botId}/upload`, { method: 'POST', headers: { 'Authorization': token }, body: formData }); if (!response.ok) { throw new Error(`Upload failed: ${response.status}`); } const data = await response.json(); console.log('File uploaded:', data); // Get saved media server configuration from localStorage let mediaConfig = { primaryService: 'nip94', primaryURL: '', fallbackService: '', fallbackURL: '' }; try { const savedConfig = localStorage.getItem('mediaConfig'); if (savedConfig) { mediaConfig = JSON.parse(savedConfig); console.log('Retrieved media config from localStorage:', mediaConfig); } else { console.log('No saved media config found in localStorage, using defaults'); } } catch (error) { console.error('Error parsing saved media config:', error); } // Use the primary service and URL from the saved config const service = mediaConfig.primaryService || 'nip94'; const serverURL = service === 'nip94' ? mediaConfig.primaryURL : mediaConfig.blossomURL; console.log(`Using media service: ${service}, URL: ${serverURL}`); // Now upload to the media server with the proper URL const mediaServerResponse = await fetch(`${API_ENDPOINT}/api/content/${botId}/uploadToMediaServer`, { method: 'POST', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: data.filename, service: service, serverURL: serverURL, // Use the URL from localStorage isProfile: true // Indicate this is a profile image }) }); if (!mediaServerResponse.ok) { throw new Error(`Media server upload failed: ${mediaServerResponse.status}`); } const mediaData = await mediaServerResponse.json(); console.log('File uploaded to media server:', mediaData); // Show preview and store URL previewUploadedImage(type, mediaData.url, file.name); // Reset upload button uploadButton.disabled = false; uploadButton.innerHTML = originalButtonText; } catch (error) { console.error('Upload error:', error); alert(`Upload failed: ${error.message}`); // Reset button state const uploadButtonId = type === 'profile' ? 'uploadProfilePicture' : type === 'banner' ? 'uploadBanner' : type === 'settings-profile' ? 'uploadSettingsProfilePicture' : 'uploadSettingsBanner'; const uploadButton = document.getElementById(uploadButtonId); if (uploadButton) { uploadButton.disabled = false; uploadButton.innerHTML = ' Upload'; } } } // Preview uploaded image function previewUploadedImage(type, imageUrl, filename) { switch(type) { case 'profile': profilePreviewImage.src = imageUrl; profilePictureUrl.textContent = filename; profilePicturePreview.classList.remove('d-none'); profileImageURL = imageUrl; break; case 'banner': bannerPreviewImage.src = imageUrl; bannerUrl.textContent = filename; bannerPreview.classList.remove('d-none'); bannerImageURL = imageUrl; break; case 'settings-profile': settingsProfilePreviewImage.src = imageUrl; settingsProfilePictureUrl.textContent = filename; settingsProfilePictureContainer.classList.remove('d-none'); settingsProfilePictureEmpty.classList.add('d-none'); settingsProfileImageURL = imageUrl; break; case 'settings-banner': settingsBannerPreviewImage.src = imageUrl; settingsBannerUrl.textContent = filename; settingsBannerContainer.classList.remove('d-none'); settingsBannerEmpty.classList.add('d-none'); settingsBannerImageURL = imageUrl; break; } } /* ---------------------------------------------------- * 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 website = document.getElementById('botWebsite')?.value.trim() || ''; // New field const keyChoice = keyOption ? keyOption.value : 'generate'; // fallback // Validate form if (!name) { alert('Bot name is required.'); return; } // Build request data with added profile/banner fields const requestData = { name: name, display_name: displayName, bio: bio, nip05: nip05, website: website, // Add website field profile_picture: profileImageURL, // Add profile image URL banner: bannerImageURL // Add banner image URL }; // 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(); } // Reset image URLs profileImageURL = ''; bannerImageURL = ''; // 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 || ''; // Add website field if it exists const websiteField = document.getElementById('botSettingsWebsite'); if (websiteField) { websiteField.value = bot.website || ''; } // Fill image fields if (settingsProfilePictureContainer && settingsProfilePictureEmpty && settingsProfilePreviewImage && settingsProfilePictureUrl) { if (bot.profile_picture) { settingsProfilePreviewImage.src = bot.profile_picture; settingsProfilePictureUrl.textContent = 'Current profile picture'; settingsProfilePictureContainer.classList.remove('d-none'); settingsProfilePictureEmpty.classList.add('d-none'); settingsProfileImageURL = bot.profile_picture; } else { settingsProfilePictureContainer.classList.add('d-none'); settingsProfilePictureEmpty.classList.remove('d-none'); settingsProfileImageURL = ''; } } // Banner if (settingsBannerContainer && settingsBannerEmpty && settingsBannerPreviewImage && settingsBannerUrl) { if (bot.banner) { settingsBannerPreviewImage.src = bot.banner; settingsBannerUrl.textContent = 'Current banner'; settingsBannerContainer.classList.remove('d-none'); settingsBannerEmpty.classList.add('d-none'); settingsBannerImageURL = bot.banner; } else { settingsBannerContainer.classList.add('d-none'); settingsBannerEmpty.classList.remove('d-none'); settingsBannerImageURL = ''; } } // If post_config is present if (bot.post_config) { document.getElementById('botSettingsInterval').value = bot.post_config.interval_minutes || 60; // Parse hashtags try { const hashtags = JSON.parse(bot.post_config.hashtags || '[]'); document.getElementById('botSettingsHashtags').value = hashtags.join(', '); } catch (e) { console.error('Failed to parse hashtags', e); document.getElementById('botSettingsHashtags').value = ''; } // Set post mode if present const postModeSelect = document.getElementById('botSettingsPostMode'); if (postModeSelect && bot.post_config.post_mode) { // Default to kind20 if not specified postModeSelect.value = bot.post_config.post_mode || 'kind20'; } } // Show the modal if (typeof bootstrap !== 'undefined' && botSettingsModalEl) { if (!botSettingsModal) { botSettingsModal = new bootstrap.Modal(botSettingsModalEl); } botSettingsModal.show(); } else { console.error('Bootstrap or modal element not found'); alert('Could not open settings modal. Please check the console for errors.'); } // Store bot ID for saving document.getElementById('botSettingsSaveBtn').setAttribute('data-bot-id', botId); } catch (err) { console.error('Error loading bot settings:', err); alert('Error loading bot: ' + err.message); } }; // saveBotSettings function to include website and images 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(); // Get website field if it exists const websiteField = document.getElementById('botSettingsWebsite'); const website = websiteField ? websiteField.value.trim() : ''; // Validate required fields if (!name) { alert('Bot name is required'); return; } // Log the data we're about to send console.log('Saving bot settings:', { botId, name, display_name: displayName, bio, nip05, zap_address: zap, website, profile_picture: settingsProfileImageURL, banner: settingsBannerImageURL }); // 1) Update the basic fields (PUT /api/bots/:id) try { const token = localStorage.getItem('authToken'); // Prepare the request payload const botData = { name, display_name: displayName, bio, nip05, zap_address: zap }; // Only add these fields if they have values if (website) { botData.website = website; } if (settingsProfileImageURL) { botData.profile_picture = settingsProfileImageURL; } if (settingsBannerImageURL) { botData.banner = settingsBannerImageURL; } const updateResp = await fetch(`${API_ENDPOINT}/api/bots/${botId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': token }, body: JSON.stringify(botData) }); if (!updateResp.ok) { // Attempt to get detailed error message let errorMessage = 'Failed to update bot info'; try { const errorData = await updateResp.json(); errorMessage = errorData.error || errorMessage; } catch (e) { console.error('Failed to parse error response', e); } throw new Error(errorMessage); } } 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()) : []; // Get selected post mode const postMode = document.getElementById('botSettingsPostMode').value; // Log the post mode selected for debugging console.log('Post mode selected:', postMode); alert('Setting post mode to: ' + postMode); const configPayload = { post_config: { interval_minutes: intervalValue, hashtags: JSON.stringify(hashtagsArr), post_mode: postMode // 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(); }; async function addGlobalRelay() { const url = document.getElementById('globalRelayURL').value.trim(); const read = document.getElementById('globalRelayRead').checked; const write = document.getElementById('globalRelayWrite').checked; if (!url) { alert('Relay URL is required'); return; } try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/global-relays`, { method: 'POST', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url, read: read, write: write }) }); if (response.ok) { // Close the modal if (globalRelayModal) { globalRelayModal.hide(); } // Refresh the list fetchGlobalRelays(); } else { const errData = await response.json().catch(() => ({})); alert(`Failed to add relay: ${errData.error || 'Unknown error'}`); } } catch (error) { console.error('Error adding global relay:', error); alert(`Error adding relay: ${error.message}`); } } // Delete global relay window.deleteGlobalRelay = async function(relayId) { if (!confirm('Are you sure you want to delete this relay?')) { return; } try { const token = localStorage.getItem('authToken'); const response = await fetch(`${API_ENDPOINT}/api/global-relays/${relayId}`, { method: 'DELETE', headers: { 'Authorization': token } }); if (response.ok) { fetchGlobalRelays(); } else { const errData = await response.json().catch(() => ({})); alert(`Failed to delete relay: ${errData.error || 'Unknown error'}`); } } catch (error) { console.error('Error deleting global relay:', error); alert(`Error deleting relay: ${error.message}`); } }; /* ---------------------------------------------------- * 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; });