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