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%)`;
}
}
/* ----------------------------------------------------
* 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 = ``;
for (const f of files) {
listHtml += `
-
${f}
`;
}
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;
});