1714 lines
74 KiB
JavaScript

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;
const API_ENDPOINT = 'http://localhost:8765'; // <--- 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 (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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-eye-slash" viewBox="0 0 16 16">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028
7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0
3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83
1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
<path d="M11.297 9.176a3.5 3.5 0 0
0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829
2.829l.822.822zm-2.943 1.299.822.822a3.5
3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0
0 2.829 2.829z"/>
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134
13.134 0 0 0 1.172 8l.195.288c.335.48.83
1.12 1.465 1.755C4.121 11.332 5.881 12.5 8
12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029
7.029 0 0 1 8 13.5C3 13.5 0 8 0
8s.939-1.721 2.641-3.238l.708.709zm10.296
8.884-12-12 .708-.708 12 12-.708.708z"/>
</svg>
`;
} else {
nsecKeyField.type = 'password';
this.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0
8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133
13.133 0 0 1 1.66-2.043C4.12 4.668 5.88
3.5 8 3.5c2.12 0 3.879 1.168 5.168
2.457A13.133 13.133 0 0 1 14.828
8c-.058.087-.122.183-.195.288-.335.48-.83
1.12-1.465 1.755C11.879 11.332 10.119
12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134
13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0
5 2.5 2.5 0 0 0 0-5zM4.5
8a3.5 3.5 0 1 1 7 0 3.5 3.5
0 0 1-7 0z"/>
</svg>
`;
}
});
}
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 = `
<div class="col-12">
<div class="card">
<div class="card-body text-center py-5">
<!-- Robot Icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"
fill="#9370DB" class="bi bi-robot mb-3" viewBox="0 0 16 16">
<path d="M6 12.5a.5.5 0 0 1
.5-.5h3a.5.5 0 0 1 0
1h-3a.5.5 0 0 1-.5-.5ZM3
8.062C3 6.76 4.235 5.765
5.53 5.886a26.58 26.58
0 0 0 4.94 0C11.765
5.765 13 6.76 13
8.062v1.157a.933.933 0
0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895
0-3.39-.2-4.235-.346A.933.933 0 0 1
3 9.219V8.062Zm4.542-.827a.25.25
0 0 0-.217.068l-.92.9a24.767
24.767 0 0 1-1.871-.183.25.25
0 0 0-.068.495c.55.076
1.232.149 2.02.193a.25.25
0 0 0 .189-.071l.754-.736.847
1.71a.25.25 0 0 0
.404.062l.932-.97a25.286
25.286 0 0 0 1.922-.188.25.25
0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25
0 0 0-.166.076l-.754.785-.842-1.7a.25.25
0 0 0-.182-.135Z"/>
<path d="M8.5 1.866a1
1 0 1 0-1
0V3h-2A4.5 4.5 0 0 0 1
7.5V8a1 1 0 0 0-1
1v2a1 1 0 0 0 1
1v1a2 2 0 0 0 2
2h10a2 2 0 0 0
2-2v-1a1 1 0 0 0 1-1V9a1
1 0 0 0-1-1v-.5A4.5 4.5
0 0 0 10.5 3h-2V1.866ZM14
7.5V13a1 1 0 0 1-1
1H3a1 1 0 0 1-1-1V7.5A3.5
3.5 0 0 1 5.5 4h5A3.5
3.5 0 0 1 14 7.5Z"/>
</svg>
<p class="lead">You don't have any bots yet.</p>
<button class="btn btn-primary mt-3"
onclick="document.getElementById('create-bot-btn').click()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-plus-lg me-1"
viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8 2a.5.5 0 0 1
.5.5v5h5a.5.5 0 0 1 0
1h-5v5a.5.5 0 0 1-1
0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5
0 0 1 8 2Z"/>
</svg>
Create Your First Bot
</button>
</div>
</div>
</div>
`;
return;
}
bots.forEach(bot => {
let statusBadge = '';
if (bot.post_config?.enabled) {
statusBadge = '<span class="badge bg-success">Active</span>';
} else {
statusBadge = '<span class="badge bg-secondary">Inactive</span>';
}
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 = `
<div class="card bot-card h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="me-3"
style="width: 50px; height: 50px; background-color: ${profileColor};
border-radius: 8px; display: flex; align-items: center;
justify-content: center; color: white; font-weight: bold;">
${initials}
</div>
<div>
<h5 class="card-title mb-0">${bot.name}</h5>
<div class="d-flex align-items-center">
<small class="text-muted">${bot.display_name || ''}</small>
<div class="ms-2">${statusBadge}</div>
</div>
</div>
</div>
<p class="card-text truncate">${bot.bio || 'No bio'}</p>
<p class="pubkey">npub...${bot.pubkey.substring(bot.pubkey.length - 8)}</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between">
<button class="btn btn-sm btn-primary view-bot" data-id="${bot.id}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-eye-fill me-1"
viewBox="0 0 16 16">
<path d="M10.5 8a2.5 2.5 0 1
1-5 0 2.5 2.5 0 0
1 5 0z"/>
<path d="M0 8s3-5.5 8-5.5S16
8 16 8s-3 5.5-8 5.5S0
8 0 8zm8 3.5a3.5 3.5 0
1 0 0-7 3.5 3.5 0 0 0
0 7z"/>
</svg>
View
</button>
<!-- For enabling/disabling -->
${bot.post_config?.enabled
? `<button class="btn btn-sm btn-outline-danger" onclick="disableBot(${bot.id})">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-pause-fill me-1"
viewBox="0 0 16 16">
<path d="M5.5 3.5A1.5 1.5 0 0 1
7 5v6a1.5 1.5 0 0 1-3
0V5a1.5 1.5 0 0 1
1.5-1.5zm5 0A1.5 1.5
0 0 1 12 5v6a1.5 1.5
0 0 1-3 0V5a1.5 1.5
0 0 1 1.5-1.5z"/>
</svg>
Pause
</button>`
: `<button class="btn btn-sm btn-outline-success" onclick="enableBot(${bot.id})">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-play-fill me-1"
viewBox="0 0 16 16">
<path d="m11.596 8.697-6.363
3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01
1.233-.696l6.363 3.692a.802.802 0 0 1 0
1.393z"/>
</svg>
Enable
</button>`
}
</div>
<!-- ADD DELETE BUTTON BELOW, for example: -->
<div class="mt-2">
<button class="btn btn-sm btn-danger" onclick="deleteBot(${bot.id})">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-trash me-1"
viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6
5h4a.5.5 0 0 1 0 1H6a.5.5 0
0 1-.5-.5zm2 3a.5.5 0 0 1
.5-.5h1a.5.5 0 0 1 0 1h-1a.5.5
0 0 1-.5-.5z"/>
<path d="M14 3a1 1 0 0 1-1
1H3a1 1 0 0 1-1-1H0v1a2
2 0 0 0 2 2v9a2 2 0 0 0
2 2h8a2 2 0 0 0 2-2V6a2
2 0 0 0 2-2V3h-2zM2.5
5h11v9a1 1 0 0 1-1
1h-9a1 1 0 0
1-1-1V5z"/>
</svg>
Delete
</button>
</div>
</div>
</div>
`;
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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="${modalId}Label">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill text-success me-2" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
Profile Published Successfully
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Your bot's profile has been published to Nostr relays!</p>
<div class="mt-3">
<label class="form-label">Note ID (NIP-19):</label>
<div class="input-group mb-2">
<input type="text" class="form-control font-monospace small" value="${eventData.note || ''}" readonly>
<button class="btn btn-outline-secondary copy-btn" data-value="${eventData.note || ''}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</button>
</div>
</div>
<div class="mt-3">
<label class="form-label">Event with Relay Info (NIP-19):</label>
<div class="input-group mb-2">
<input type="text" class="form-control font-monospace small" value="${eventData.nevent || ''}" readonly>
<button class="btn btn-outline-secondary copy-btn" data-value="${eventData.nevent || ''}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</button>
</div>
</div>
<div class="alert alert-info mt-3">
<small>
<strong>Tip:</strong> You can use these identifiers to share your bot's profile or look it up in Nostr clients.
</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
`;
// 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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check text-success" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
`;
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 = '<div class="alert alert-warning">Failed to load global relays</div>';
}
} catch (error) {
console.error('Error fetching global relays:', error);
globalRelaysList.innerHTML = '<div class="alert alert-danger">Error loading global relays</div>';
}
}
// Render global relays
function renderGlobalRelays(relays) {
if (!globalRelaysList) return;
globalRelaysList.innerHTML = '';
if (!relays || relays.length === 0) {
globalRelaysList.innerHTML = `
<div class="alert alert-info">
No global relays configured. Add a relay to get started.
</div>
`;
return;
}
let html = '<div class="list-group">';
relays.forEach(relay => {
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">${relay.url}</h5>
<span class="badge ${relay.read ? 'bg-primary' : 'bg-secondary'} me-1">Read: ${relay.read ? 'Yes' : 'No'}</span>
<span class="badge ${relay.write ? 'bg-primary' : 'bg-secondary'}">Write: ${relay.write ? 'Yes' : 'No'}</span>
</div>
<div>
<button class="btn btn-sm btn-danger" onclick="deleteGlobalRelay(${relay.id})">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 5h4a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5zm2 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5z"/>
<path d="M14 3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1H0v1a2 2 0 0 0 2 2v9a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0 2-2V3h-2zM2.5 5h11v9a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1V5z"/>
</svg>
</button>
</div>
</div>
</div>
`;
});
html += '</div>';
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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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 = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z"/><path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/></svg> 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 = `<option value="">-- Select a Bot --</option>`;
bots.forEach(bot => {
botSelect.innerHTML += `<option value="${bot.id}">${bot.name} (ID: ${bot.id})</option>`;
});
} 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 = `
<h3>Content for Bot #${botId}</h3>
<div class="mb-3">
<input type="file" id="uploadFileInput" />
<button class="btn btn-primary" onclick="uploadBotFile(${botId})">Upload</button>
</div>
`;
if (!files || files.length === 0) {
container.innerHTML += `<p>No files.</p>`;
return;
}
// Show a simple list
let listHtml = `<ul class="list-group">`;
for (const f of files) {
listHtml += `
<li class="list-group-item d-flex justify-content-between align-items-center">
${f}
<button class="btn btn-sm btn-danger" onclick="deleteBotFile(${botId}, '${f}')">Delete</button>
</li>
`;
}
listHtml += `</ul>`;
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;
});