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.
Create Your First Bot
`;
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 = `
Your bot's profile has been published to Nostr relays!
Event with Relay Info (NIP-19):
Tip: You can use these identifiers to share your bot's profile or look it up in Nostr clients.
`;
// 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 = `-- Select a Bot -- `;
bots.forEach(bot => {
botSelect.innerHTML += `${bot.name} (ID: ${bot.id}) `;
});
} 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}
Upload
`;
if (!files || files.length === 0) {
container.innerHTML += `No files.
`;
return;
}
// Show a simple list
let listHtml = ``;
for (const f of files) {
listHtml += `
${f}
Delete
`;
}
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;
});