351 lines
16 KiB
JavaScript
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"/>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
}); |