351 lines
16 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');
const generateKeypair = document.getElementById('generateKeypair');
const keypairInput = document.getElementById('keypair-input');
// Bootstrap Modal
let createBotModal;
if (typeof bootstrap !== 'undefined') {
createBotModal = new bootstrap.Modal(document.getElementById('createBotModal'));
}
// State
let currentUser = null;
const API_ENDPOINT = '';
// Check if already logged in
checkAuth();
// Event Listeners
loginButton.addEventListener('click', login);
logoutButton.addEventListener('click', logout);
createBotBtn.addEventListener('click', showCreateBotModal);
saveBotBtn.addEventListener('click', createBot);
generateKeypair.addEventListener('change', toggleKeypairInput);
// Functions
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();
currentUser = data.pubkey;
showMainContent();
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();
}
function showAuthSection() {
authSection.classList.remove('d-none');
mainContent.classList.add('d-none');
loginButton.classList.remove('d-none');
userInfo.classList.add('d-none');
logoutButton.classList.add('d-none');
}
function showMainContent() {
authSection.classList.add('d-none');
mainContent.classList.remove('d-none');
loginButton.classList.add('d-none');
userInfo.classList.remove('d-none');
logoutButton.classList.remove('d-none');
// Truncate pubkey for display
const shortPubkey = currentUser.substring(0, 8) + '...' + currentUser.substring(currentUser.length - 4);
userPubkey.textContent = shortPubkey;
}
async function fetchBots() {
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) {
botsList.innerHTML = '';
if (bots.length === 0) {
botsList.innerHTML = `
<div class="col-12">
<div class="card">
<div class="card-body text-center py-5">
<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 => {
// Create a status badge
let statusBadge = '';
if (bot.post_config?.enabled) {
statusBadge = '<span class="badge bg-success">Active</span>';
} else {
statusBadge = '<span class="badge bg-secondary">Inactive</span>';
}
// Generate a profile image based on the bot's pubkey (just a colored square)
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>
<button class="btn btn-sm ${bot.post_config?.enabled ? 'btn-outline-danger' : 'btn-outline-success'}">
${bot.post_config?.enabled ?
'<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' :
'<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>
</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%)`;
}
}
function showCreateBotModal() {
// Reset form
document.getElementById('create-bot-form').reset();
generateKeypair.checked = true;
keypairInput.classList.add('d-none');
// Show modal
createBotModal.show();
}
function toggleKeypairInput() {
if (generateKeypair.checked) {
keypairInput.classList.add('d-none');
} else {
keypairInput.classList.remove('d-none');
}
}
async function createBot() {
const name = document.getElementById('botName').value;
const displayName = document.getElementById('botDisplayName').value;
const bio = document.getElementById('botBio').value;
const nip05 = document.getElementById('botNip05').value;
if (!name) {
alert('Bot name is required.');
return;
}
let pubkey = '';
let privkey = '';
if (!generateKeypair.checked) {
pubkey = document.getElementById('botPubkey').value;
privkey = document.getElementById('botPrivkey').value;
if (!pubkey || !privkey) {
alert('Both public and private keys are required when not generating a keypair.');
return;
}
}
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({
name,
display_name: displayName,
bio,
nip05,
pubkey,
encrypted_privkey: privkey
})
});
if (response.ok) {
createBotModal.hide();
fetchBots();
} else {
const error = await response.json();
alert(`Failed to create bot: ${error.error}`);
}
} catch (error) {
console.error('Error creating bot:', error);
alert('Error creating bot: ' + error.message);
}
}
const togglePrivkeyBtn = document.getElementById('togglePrivkey');
if (togglePrivkeyBtn) {
togglePrivkeyBtn.addEventListener('click', function() {
const privkeyInput = document.getElementById('botPrivkey');
const eyeIcon = this.querySelector('svg');
if (privkeyInput.type === 'password') {
privkeyInput.type = 'text';
eyeIcon.innerHTML = `
<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"/>
`;
} else {
privkeyInput.type = 'password';
eyeIcon.innerHTML = `
<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"/>
`;
}
});
}
});