15 Commits

Author SHA1 Message Date
58e2ae95c0 Update web/superball.html 2025-11-30 03:47:37 +00:00
792eb46ca2 Update web/superball-shared.css 2025-11-24 03:49:18 +00:00
ee5f318532 Update web/superball.html 2025-11-24 03:48:39 +00:00
59b0461bad Added custom events and updated reply events handling
Implemented support for creating custom events. Also integrated functionality to generate reply events by retrieving them from relays first, with compatibility for multiple eventId formats like nevent, note, and hexadecimal strings.
2025-11-22 11:24:50 +00:00
9285f8e583 Update web/superball.html 2025-11-18 04:11:37 +00:00
c5ec067a79 Update web/superball.html 2025-11-18 03:54:22 +00:00
387f5d4725 Update web/superball.html 2025-11-18 02:37:28 +00:00
c4d73026b5 CSS 2025-11-16 04:38:00 +00:00
516c4f2f80 Adds reply and profile creation along with post functionality
This commit introduces new features to enhance user interaction and profile management:

- Adds the ability to create replies for comments
- Enables profile creation for users
- Supports post creation for sharing updates and content
2025-11-16 04:36:10 +00:00
Your Name
6ff47cec92 Fixed bugs with install script 2025-09-29 10:41:04 -04:00
Your Name
5b1fa185f7 . 2025-09-29 10:30:31 -04:00
Your Name
d941638779 . 2025-09-29 09:57:36 -04:00
Your Name
e6bef22494 . 2025-09-29 09:39:14 -04:00
Your Name
3edc477069 install.sh 2025-09-29 09:28:19 -04:00
Your Name
5b618c08ed fix ws issue 2025-09-29 07:36:12 -04:00
6 changed files with 656 additions and 130 deletions

View File

@@ -418,7 +418,7 @@ class EventProcessor {
if (!routing.audit || typeof routing.audit !== 'string') return false; if (!routing.audit || typeof routing.audit !== 'string') return false;
// Check maximum delay limit // Check maximum delay limit
const maxDelay = this.config.get('thrower.maxDelay') || 3600; const maxDelay = this.config.get('thrower.maxDelay') || 86460;
if (routing.delay > maxDelay) { if (routing.delay > maxDelay) {
this.logger.error(`Routing delay ${routing.delay}s exceeds maximum allowed delay of ${maxDelay}s`); this.logger.error(`Routing delay ${routing.delay}s exceeds maximum allowed delay of ${maxDelay}s`);
return false; return false;
@@ -648,7 +648,7 @@ class ThrowerInfoManager {
if (throwerInfo.privacyPolicy) tags.push(['privacy_policy', throwerInfo.privacyPolicy]); if (throwerInfo.privacyPolicy) tags.push(['privacy_policy', throwerInfo.privacyPolicy]);
if (throwerInfo.termsOfService) tags.push(['terms_of_service', throwerInfo.termsOfService]); if (throwerInfo.termsOfService) tags.push(['terms_of_service', throwerInfo.termsOfService]);
tags.push(['refresh_rate', throwerInfo.refreshRate.toString()]); tags.push(['refresh_rate', throwerInfo.refreshRate.toString()]);
tags.push(['max_delay', (throwerInfo.maxDelay || 3600).toString()]); tags.push(['max_delay', (throwerInfo.maxDelay || 86460).toString()]);
const eventTemplate = { const eventTemplate = {
kind: 12222, kind: 12222,
@@ -933,7 +933,7 @@ class WebSocketManager {
this.logger.info('Stopping WebSocket monitoring...'); this.logger.info('Stopping WebSocket monitoring...');
this.connections.forEach(({ url, ws }) => { this.connections.forEach(({ url, ws }) => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === ws.OPEN) {
// Send CLOSE message for subscription // Send CLOSE message for subscription
if (this.subscriptionId) { if (this.subscriptionId) {
const closeMsg = JSON.stringify(['CLOSE', this.subscriptionId]); const closeMsg = JSON.stringify(['CLOSE', this.subscriptionId]);

View File

@@ -5,126 +5,261 @@
set -e set -e
echo "=== Superball Thrower Daemon Installation ===" echo "🏀 Superball Thrower Daemon Installation"
echo echo "========================================"
# Check if running as root # Check if running as root
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
echo "This script should not be run as root for security reasons." echo "This script should not be run as root for security reasons"
echo "Please run as a regular user with sudo privileges." echo " Please run as a regular user with sudo privileges"
exit 1 exit 1
fi fi
# Check if Node.js is installed # Function to check if command exists
if ! command -v node &> /dev/null; then command_exists() {
echo "Error: Node.js is not installed." command -v "$1" >/dev/null 2>&1
echo "Please install Node.js 16 or later from https://nodejs.org/" }
# Check for required system dependencies
echo "📋 Checking system dependencies..."
# Check for curl
if ! command_exists curl; then
echo "❌ curl is required but not installed"
echo " Please install curl: sudo apt update && sudo apt install curl"
exit 1 exit 1
fi fi
# Check Node.js version # Check for systemctl (systemd)
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) if ! command_exists systemctl; then
if [ "$NODE_VERSION" -lt 16 ]; then echo "❌ systemd is required but not found"
echo "Error: Node.js version 16 or later is required." echo " This script requires a systemd-based system"
echo "Current version: $(node --version)"
exit 1 exit 1
fi fi
echo "✓ Node.js $(node --version) detected" # Install latest Node.js using nvm
echo "🔧 Installing latest Node.js using nvm..."
# Check if npm is installed # Download and install nvm
if ! command -v npm &> /dev/null; then if [ ! -d "$HOME/.nvm" ]; then
echo "Error: npm is not installed." echo " Downloading and installing nvm..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
fi
# Load nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
# Install Node.js 22 (latest LTS)
echo " Installing Node.js 22..."
nvm install 22
nvm use 22
# Verify installation
NODE_VERSION=$(node --version)
NPM_VERSION=$(npm --version)
echo "✅ Node.js $NODE_VERSION installed"
echo "✅ npm $NPM_VERSION installed"
# Copy Node.js binaries to system-wide location
echo "📋 Installing Node.js binaries system-wide..."
NODE_PATH=$(which node)
NPM_PATH=$(which npm)
if [ -n "$NODE_PATH" ] && [ -x "$NODE_PATH" ]; then
# Remove any existing symlinks first
sudo rm -f /usr/local/bin/node
sudo cp "$NODE_PATH" /usr/local/bin/node
sudo chmod +x /usr/local/bin/node
echo "✅ Node.js binary copied to /usr/local/bin/node"
else
echo "❌ Failed to find Node.js executable"
exit 1 exit 1
fi fi
echo "✓ npm $(npm --version) detected" if [ -n "$NPM_PATH" ] && [ -x "$NPM_PATH" ]; then
# Remove any existing symlinks first
# Get the directory where this script is located sudo rm -f /usr/local/bin/npm
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" sudo cp "$NPM_PATH" /usr/local/bin/npm
DAEMON_DIR="$SCRIPT_DIR" sudo chmod +x /usr/local/bin/npm
echo "✅ npm binary copied to /usr/local/bin/npm"
echo "Installing to: $DAEMON_DIR" else
echo echo "❌ Failed to find npm executable"
# Install Node.js dependencies
echo "Installing Node.js dependencies..."
cd "$DAEMON_DIR"
npm install
if [ $? -ne 0 ]; then
echo "Error: Failed to install Node.js dependencies"
exit 1 exit 1
fi fi
echo "✓ Dependencies installed successfully" # Install npm dependencies
echo echo "📦 Installing npm dependencies..."
if ! npm install; then
echo "❌ Failed to install npm dependencies"
exit 1
fi
# Create user for daemon (if it doesn't exist) echo "✅ Dependencies installed successfully"
DAEMON_USER="superball"
if ! id "$DAEMON_USER" &>/dev/null; then # Interactive configuration
echo "Creating system user: $DAEMON_USER" echo ""
echo "⚙️ Thrower Configuration"
echo "========================"
echo "Please provide the following information for your Superball Thrower:"
echo ""
# Prompt for configuration values
read -p "Enter your private key (hex format, leave empty to generate new): " PRIVATE_KEY
read -p "Enter your public key (hex format, leave empty if generating new): " PUBLIC_KEY
read -p "Enter thrower name (e.g., 'My Superball Thrower'): " THROWER_NAME
read -p "Enter thrower description: " THROWER_DESCRIPTION
read -p "Enter banner URL (optional, press enter to skip): " BANNER_URL
read -p "Enter icon URL (optional, press enter to skip): " ICON_URL
# Generate keypair if not provided
if [ -z "$PRIVATE_KEY" ] || [ -z "$PUBLIC_KEY" ]; then
echo "🔑 Generating new keypair..."
# We'll let the daemon generate the keypair on first run
PRIVATE_KEY=""
PUBLIC_KEY=""
fi
# Update config.json with user input
echo "📝 Updating configuration file..."
# Create a temporary config file with user values
cat > config.json << EOF
{
"thrower": {
"privateKey": "$PRIVATE_KEY",
"publicKey": "$PUBLIC_KEY",
"name": "$THROWER_NAME",
"description": "$THROWER_DESCRIPTION",
"banner": "$BANNER_URL",
"icon": "$ICON_URL",
"adminPubkey": "",
"contact": "",
"supportedSups": "1,2,3,4,5,6",
"software": "https://git.laantungir.net/laantungir/super_ball.git",
"version": "1.0.0",
"privacyPolicy": "",
"termsOfService": "",
"refreshRate": 300,
"maxDelay": 86460
},
"relays": [
{ "url": "wss://relay.laantungir.net", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://relay.damus.io", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://nos.lol", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://relay.snort.social", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://relay.primal.net", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://relay.nostr.band", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://nostr.oxtr.dev", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://offchain.pub", "read": true, "write": true, "authStatus": "unknown" },
{ "url": "wss://nostr-pub.wellorder.net", "read": true, "write": true, "authStatus": "unknown" }
],
"daemon": {
"logLevel": "info",
"maxQueueSize": 1000,
"maxLogEntries": 1000,
"autoStart": false
}
}
EOF
echo "✅ Configuration updated"
# Create system user for the daemon
echo "👤 Creating system user..."
DAEMON_USER="superball-thrower"
if id "$DAEMON_USER" &>/dev/null; then
echo "✅ User $DAEMON_USER already exists"
else
echo " Creating user: $DAEMON_USER"
sudo useradd --system --no-create-home --shell /bin/false "$DAEMON_USER" sudo useradd --system --no-create-home --shell /bin/false "$DAEMON_USER"
echo " User $DAEMON_USER created" echo " User $DAEMON_USER created"
else
echo "✓ User $DAEMON_USER already exists"
fi fi
# Create directories # Create installation directory
echo "Creating directories..." INSTALL_DIR="/opt/superball-thrower"
sudo mkdir -p /var/log/superball echo "📁 Setting up installation directory: $INSTALL_DIR"
sudo mkdir -p /etc/superball
sudo chown "$DAEMON_USER:$DAEMON_USER" /var/log/superball
echo "✓ Directories created"
# Copy configuration file sudo mkdir -p "$INSTALL_DIR"
if [ ! -f "/etc/superball/config.json" ]; then sudo cp daemon.js "$INSTALL_DIR/"
echo "Installing default configuration..." sudo cp package.json "$INSTALL_DIR/"
sudo cp "$DAEMON_DIR/config.json" /etc/superball/config.json sudo cp config.json "$INSTALL_DIR/"
sudo chown "$DAEMON_USER:$DAEMON_USER" /etc/superball/config.json
sudo chmod 600 /etc/superball/config.json # Copy node_modules if it exists
echo "✓ Configuration installed to /etc/superball/config.json" if [ -d "node_modules" ]; then
echo "⚠️ IMPORTANT: Edit /etc/superball/config.json with your private key and settings" sudo cp -r node_modules "$INSTALL_DIR/"
else
echo "✓ Configuration already exists at /etc/superball/config.json"
fi fi
# Set ownership and permissions
sudo chown -R "$DAEMON_USER:$DAEMON_USER" "$INSTALL_DIR"
sudo chmod 755 "$INSTALL_DIR"
sudo chmod 644 "$INSTALL_DIR"/*.js "$INSTALL_DIR"/*.json
sudo chmod -R 755 "$INSTALL_DIR/node_modules" 2>/dev/null || true
echo "✅ Files installed to $INSTALL_DIR"
# Create log directory
LOG_DIR="/var/log/superball-thrower"
echo "📝 Creating log directory: $LOG_DIR"
sudo mkdir -p "$LOG_DIR"
sudo chown "$DAEMON_USER:$DAEMON_USER" "$LOG_DIR"
sudo chmod 755 "$LOG_DIR"
echo "✅ Log directory created"
# Install systemd service # Install systemd service
echo "Installing systemd service..." echo "⚙️ Installing systemd service..."
sudo cp "$DAEMON_DIR/superball-thrower.service" /etc/systemd/system/
sudo sed -i "s|/path/to/thrower_daemon|$DAEMON_DIR|g" /etc/systemd/system/superball-thrower.service sudo cp superball-thrower.service /etc/systemd/system/
sudo systemctl daemon-reload sudo systemctl daemon-reload
echo "✓ Systemd service installed" sudo systemctl enable superball-thrower.service
# Set permissions echo "✅ Systemd service installed and enabled"
echo "Setting permissions..."
sudo chown -R "$DAEMON_USER:$DAEMON_USER" "$DAEMON_DIR"
sudo chmod +x "$DAEMON_DIR/daemon.js"
echo "✓ Permissions set"
echo # Initialize daemon if no keypair was provided
echo "=== Installation Complete ===" if [ -z "$PRIVATE_KEY" ]; then
echo echo "🔑 Initializing daemon to generate keypair..."
echo "Next steps:" # Create a simple init script that generates keypair
echo "1. Edit the configuration file:" sudo -u "$DAEMON_USER" bash -c "
echo " sudo nano /etc/superball/config.json" cd \"$INSTALL_DIR\"
echo /usr/local/bin/node -e \"
echo "2. Add your private key and configure relays" const { generateSecretKey, getPublicKey } = require('nostr-tools/pure');
echo const fs = require('fs');
echo "3. Enable and start the service:" const config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
echo " sudo systemctl enable superball-thrower" const sk = generateSecretKey();
echo " sudo systemctl start superball-thrower" const pk = getPublicKey(sk);
echo config.thrower.privateKey = Buffer.from(sk).toString('hex');
echo "4. Check service status:" config.thrower.publicKey = pk;
echo " sudo systemctl status superball-thrower" fs.writeFileSync('config.json', JSON.stringify(config, null, 2));
echo console.log('Generated keypair:');
echo "5. View logs:" console.log('Private key:', Buffer.from(sk).toString('hex'));
echo " sudo journalctl -u superball-thrower -f" console.log('Public key:', pk);
echo \"
echo "Configuration file location: /etc/superball/config.json" "
echo "Log file location: /var/log/superball/daemon.log" echo "✅ Keypair generated"
echo "Service name: superball-thrower" fi
echo
echo "⚠️ SECURITY WARNING:" # Installation complete
echo " Make sure to secure your private key in the configuration file!" echo ""
echo " The config file should only be readable by the superball user." echo "🎉 Installation completed successfully!"
echo ""
echo "📋 Next steps:"
echo " 1. Start the service: sudo systemctl start superball-thrower"
echo " 2. Check status: sudo systemctl status superball-thrower"
echo " 3. View logs: sudo journalctl -u superball-thrower -f"
echo ""
echo "📖 For more information, see the README.md file"
echo ""
echo "🔐 Security notes:"
echo " - The daemon runs as user '$DAEMON_USER' with limited privileges"
echo " - Configuration files are owned by the daemon user"
echo " - Logs are written to $LOG_DIR"
echo ""
echo "⚙️ Configuration summary:"
echo " - Thrower name: $THROWER_NAME"
echo " - Description: $THROWER_DESCRIPTION"
echo " - Config file: $INSTALL_DIR/config.json"
echo ""

View File

@@ -3,9 +3,19 @@
# Superball Thrower Daemon Installation Script # Superball Thrower Daemon Installation Script
# This script installs and configures the Superball Thrower daemon # This script installs and configures the Superball Thrower daemon
sudo apt install nodejs npm # Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 22
# Verify the Node.js version:
node -v # Should print "v22.20.0".
# Verify npm version:
npm -v # Should print "10.9.3".
# Verify installation
node --version
npm --version

View File

@@ -6,10 +6,11 @@ Wants=network-online.target
[Service] [Service]
Type=simple Type=simple
User=superball User=superball-thrower
Group=superball Group=superball-thrower
WorkingDirectory=/path/to/thrower_daemon WorkingDirectory=/opt/superball-thrower
ExecStart=/usr/bin/node daemon.js /etc/superball/config.json ExecStart=/usr/local/bin/node daemon.js start
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Restart=always Restart=always
RestartSec=10 RestartSec=10
StandardOutput=journal StandardOutput=journal
@@ -21,14 +22,14 @@ NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
ProtectSystem=strict ProtectSystem=strict
ProtectHome=true ProtectHome=true
ReadWritePaths=/var/log/superball ReadWritePaths=/var/log/superball-thrower /opt/superball-thrower
ProtectKernelTunables=true ProtectKernelTunables=true
ProtectKernelModules=true ProtectKernelModules=true
ProtectControlGroups=true ProtectControlGroups=true
RestrictRealtime=true RestrictRealtime=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
LockPersonality=true LockPersonality=true
MemoryDenyWriteExecute=true # MemoryDenyWriteExecute=true # Disabled - conflicts with Node.js JIT compilation
RestrictNamespaces=true RestrictNamespaces=true
SystemCallFilter=@system-service SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM SystemCallErrorNumber=EPERM

View File

@@ -142,7 +142,8 @@ button:active {
label { label {
display: block; display: block;
font-weight: bold; font-weight: bold;
margin-bottom: 3px; margin-bottom: 2px;
margin-top: 7px;
font-family: var(--font-family); font-family: var(--font-family);
color: var(--primary-color); color: var(--primary-color);
} }
@@ -216,11 +217,11 @@ label {
@keyframes pulse { @keyframes pulse {
0%, 0%,
100% { 100% {
opacity: 1; opacity: 1;
} }
50% { 50% {
opacity: 0.5; opacity: 0.5;
} }
} }
@@ -737,4 +738,66 @@ small {
.floating-tab:hover { .floating-tab:hover {
transform: scale(1.05); transform: scale(1.05);
} }
/* .input-group {
margin: 20px;
} */
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.tabs {
display: flex;
margin: 20px 20px 0;
border-bottom: 1px solid #ccc;
}
.tab {
padding: 10px 20px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-bottom: none;
margin-right: 5px;
border-radius: 4px 4px 0 0;
}
.tab.active {
background-color: #fff;
border-bottom: 1px solid #fff;
}
.tab-content {
display: none;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}
.tab-content.active {
display: block;
}
.fetched-event {
background: var(--secondary-color);
border: 1px solid var(--muted-color);
border-radius: 4px;
padding: 10px;
font-family: var(--font-family);
font-size: 16px;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
margin: 10px 0;
color: var(--primary-color);
display: none;
}

View File

@@ -27,13 +27,63 @@
<!-- FINAL EVENT SECTION --> <!-- FINAL EVENT SECTION -->
<div class="section"> <div class="section">
<h2>Final Event (What gets posted at the end)</h2>
<div class="input-group"> <div class="input-group">
<label for="final-content">Message Content:</label> <div class="tabs">
<textarea id="final-content" rows="3" placeholder="Enter your message content..."></textarea> <div class="tab active" data-tab="tab1">Post</div>
<div class="tab" data-tab="tab2">Reply</div>
<div class="tab" data-tab="tab3">Create/Edit Profile</div>
<div class="tab" data-tab="tab4">Custom Event</div>
</div>
<div class="tab-content active" id="tab1">
<h3>Post</h3>
<label for="post-content">Message Content:</label>
<textarea id="post-content" rows="3" placeholder="Enter your message content..."></textarea>
</div>
<div class="tab-content" id="tab2">
<h3>Reply</h3>
<label for="reply-id">EventId/NoteId/Nevent:</label>
<textarea id="reply-id" placeholder="Enter the nevent for the note..."></textarea>
<label for="fetched-event-display" id="fetched-event-label" style="display: none;">Event Replying To:</label>
<div id="fetched-event-display" class="fetched-event"></div>
<label for="reply-content">Message Content:</label>
<textarea id="reply-content" rows="3" placeholder="Enter your message content..."></textarea>
</div>
<div class="tab-content" id="tab3">
<h3>Create Profile</h3>
<label for="name">Name:</label>
<textarea id="name" placeholder="Enter your name..."></textarea>
<label for="about">About:</label>
<textarea id="about" rows="2" placeholder="A short bio..."></textarea>
<label for="profile-pic">Profile Picture:</label>
<textarea id="profile-pic" placeholder="URL of your profile pic..."></textarea>
<label for="display-name">Display Name:</label>
<textarea id="display-name" placeholder="Enter your display name..."></textarea>
<label for="website">Website:</label>
<textarea id="website" placeholder="Web URL..."></textarea>
<label for="banner">Banner:</label>
<textarea id="banner" placeholder="Enter your bannerm a (~1024x768) wide picture url..."></textarea>
<label for="nip05">NIP05:</label>
<textarea id="nip05" placeholder="Enter your nip05 in the format username@domain.com..."></textarea>
<label for="lud16">Lightning Address:</label>
<textarea id="lud16" placeholder="Enter your lightning address..."></textarea>
</div>
<div class="tab-content" id="tab4">
<h3>Custom Event</h3>
<label for="kind">Event Kind:</label>
<textarea id="kind" placeholder="Enter your event kind. Ex: 0, 1, 30000..."></textarea>
<label for="content">Content:</label>
<textarea id="content" rows="3" placeholder="A short bio..."></textarea>
<label for="tags">Tags:</label>
<textarea id="tags" rows="3" placeholder="Tags in format [['p', 'pubkey...'], ['e', 'event_id', wss://nos.lol, 'pubkey...']]"></textarea>
</div>
</div> </div>
<button onclick="createFinalEvent()">Create Event That Will Be Published Publicly</button> <button onclick="createFinalEvent()">Create Event That Will Be Published Publicly</button>
<h2 style="padding-top: 16px;">Final Event (What gets posted at the end)</h2>
<div id="final-event-display" class="json-display"></div> <div id="final-event-display" class="json-display"></div>
</div> </div>
@@ -71,14 +121,34 @@
</div> </div>
<!-- Load the official nostr-tools bundle first --> <!-- Load the official nostr-tools bundle first -->
<!-- <script src="./nostr.bundle.js"></script> --> <script src="./nostr.bundle.js"></script>
<script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> <!-- <script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> -->
<!-- Load NOSTR_LOGIN_LITE main library --> <!-- Load NOSTR_LOGIN_LITE main library -->
<script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> <!-- <script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> -->
<!-- <script src="./nostr-lite.js"></script> --> <script src="./nostr-lite.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all tabs and contents
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Add active class to clicked tab
tab.classList.add('active');
// Show corresponding content
const tabId = tab.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
});
// Global variables // Global variables
let nlLite = null; let nlLite = null;
let userPubkey = null; let userPubkey = null;
@@ -190,6 +260,11 @@
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User'; const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
const about = profile.about || ''; const about = profile.about || '';
const picture = profile.picture || ''; const picture = profile.picture || '';
const display_name = profile.display_name || '';
const website = profile.website || '';
const banner = profile.banner || '';
const nip05 = profile.nip05 || '';
const lud16 = profile.lud16 || '';
document.getElementById('profile-name').textContent = name; document.getElementById('profile-name').textContent = name;
@@ -197,6 +272,17 @@
document.getElementById('profile-picture').src = picture; document.getElementById('profile-picture').src = picture;
} }
// Edit Profile section
document.getElementById('name').textContent = name;
document.getElementById('about').textContent = about;
document.getElementById('profile-pic').textContent = picture;
document.getElementById('display-name').textContent = display_name;
document.getElementById('website').textContent = website;
document.getElementById('banner').textContent = banner;
document.getElementById('nip05').textContent = nip05;
document.getElementById('lud16').textContent = lud16;
console.log('SUCCESS', `Profile displayed: ${name}`); console.log('SUCCESS', `Profile displayed: ${name}`);
} }
@@ -232,7 +318,11 @@
} else { } else {
console.log('INFO', 'No relay list found, using defaults'); console.log('INFO', 'No relay list found, using defaults');
userRelays = [ userRelays = [
{ url: 'wss://relay.laantungir.net', type: 'both' } { url: 'wss://relay.laantungir.net', type: 'both' },
{ url: 'wss://relay.primal.net', type: 'both' },
{ url: 'wss://nos.lol', type: 'both' },
{ url: 'wss://relay.damus.io', type: 'both' },
{ url: 'wss://offchain.pub', type: 'both' }
]; ];
return userRelays; return userRelays;
} }
@@ -710,24 +800,248 @@
} }
} }
// Load event from id
async function loadReplyTagsForEvent(id, knownRelays) {
if (!id) return;
console.log('INFO', `Loading event`, id);
try {
const pool = new window.NostrTools.SimplePool();
const relays = [relayUrl, 'wss://relay.laantungir.net', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.damus.io', 'wss://relay.nostr.band'].concat(knownRelays);
// Enable tracking
pool.trackRelays = true;
// Query for an event
const events = await pool.querySync(relays, {
ids: [id],
limit: 1
});
const event = events[0];
let returnEventTags = [];
let relaysSeenOn = [];
if (event) {
const seenRelays = pool.seenOn.get(event.id);
relaysSeenOn = seenRelays ? Array.from(seenRelays).map(r => r.url) : [];
}
pool.close(relays);
if (events.length > 0) {
// event is a nostr event with tags
const refs = window.NostrTools.nip10.parse(events[0])
// get the root event of the thread
if (refs.root) {
const relay = refs.root.relays.length > 0 ? refs.root.relays[0] : '';
if (refs.root.author) {
returnEventTags.push(['e', refs.root.id, relay, 'root', refs.root.author]);
}
else {
returnEventTags.push(['e', refs.root.id, relay, 'root']);
}
returnEventTags.push(['e', id, relaysSeenOn[0], 'reply', event.pubkey]);
if (refs.root.author)
returnEventTags.push(['p', refs.root.author, relay]);
returnEventTags.push(['p', event.pubkey, relaysSeenOn[0]]);
} else {
returnEventTags.push(['e', id, relaysSeenOn[0], 'root']);
returnEventTags.push(['p', event.pubkey]);
}
// get any referenced profiles
for (let profile of refs.profiles) {
if (!returnEventTags.some(tag => tag[0] === 'p' && tag[1] === profile.pubkey)) {
if (profile.relays.length > 0) {
returnEventTags.push(['p', profile.pubkey, profile.relays[0]]);
}
else {
returnEventTags.push(['p', profile.pubkey]);
}
}
}
return [events[0], returnEventTags];
} else {
console.log('INFO', 'Event not found');
return null;
}
} catch (error) {
console.log('ERROR', `Profile loading failed: ${error.message}`);
}
}
// Create final event (kind 1) // Create final event (kind 1)
async function createFinalEvent() { async function createFinalEvent() {
const content = document.getElementById('final-content').value.trim(); // Get the active tab
const activeTab = document.querySelector('.tab.active').getAttribute('data-tab');
// Get content based on active tab
let content = '';
let replyEventId = '';
let replyEvent = {};
let replyTags = [];
let name = '';
let about = '';
let profilePic = '';
let displayName = '';
let website = '';
let banner = '';
let nip05 = '';
let lud16 = '';
let tags = '';
let kind = '';
switch(activeTab) {
case 'tab1': // Post
content = document.getElementById('post-content').value.trim();
break;
case 'tab2': // Reply
content = document.getElementById('reply-content').value.trim();
replyEventId = document.getElementById('reply-id').value.trim();
break;
case 'tab3': // Create Profile
name = document.getElementById('name').value.trim();
about = document.getElementById('about').value.trim();
profilePic = document.getElementById('profile-pic').value.trim();
displayName = document.getElementById('display-name').value.trim();
website = document.getElementById('website').value.trim();
banner = document.getElementById('banner').value.trim();
nip05 = document.getElementById('nip05').value.trim();
lud16 = document.getElementById('lud16').value.trim();
break;
case 'tab4': // Custom Event
kind = document.getElementById('kind').value.trim();
content = document.getElementById('content').value.trim();
tags = document.getElementById('tags').value.trim();
break;
}
// Validate content based on tab
if (activeTab === 'tab1') {
if (!content) {
alert('Please enter message content');
return;
}
} else if (activeTab === 'tab2') {
if (!content) {
alert('Please enter message content');
return;
}
let eventId = '';
let knownRelays = [];
try {
if (replyEventId.startsWith('nevent')) {
const replyEventData = window.NostrTools.nip19.decode(replyEventId).data;
eventId = replyEventData.id;
knownRelays = replyEventData.relays;
} else if (replyEventId.startsWith('note')) {
eventId = window.NostrTools.nip19.decode(replyEventId).data;
} else {
eventId = replyEventId;
}
} catch (error) {
console.error(error);
alert('Error decoding nevent string', error.message);
return;
}
if (!content) { const regex = /^[0-9a-f]{64}$/;
alert('Please enter message content'); if (!regex.test(eventId)) {
return; alert('Invalid event ID');
}
try {
document.getElementById('fetched-event-display').textContent = 'Loading event...';
document.getElementById('fetched-event-display').style.display = 'block';
document.getElementById('fetched-event-label').style.display = 'block';
[ replyEvent, replyTags ] = await loadReplyTagsForEvent(eventId, knownRelays);
console.log("Event replying to: ", replyEvent)
document.getElementById('fetched-event-display').textContent = replyEvent.content;
} catch (error) {
alert('Error fetching reply event', error.message);
return;
}
} else if (activeTab === 'tab3') {
if (!name) {
alert('Please enter your name');
return;
}
} else if (activeTab === 'tab4') {
if (!kind) {
alert('Please enter the event kind');
return;
}
if (!/^-?\d+$/.test(kind)) {
alert("Please enter a valid integer for event kind.");
return; // Prevent form submission
}
try {
kind = Number(kind);
tags = JSON.parse(tags.replace(/'/g, '"'))
} catch (error) {
alert('Error parsing tags', error.message);
return;
}
} }
try { try {
// Create the final event (kind 1) - pure message, no relay info let eventTemplate = {};
const eventTemplate = {
kind: 1, switch(activeTab) {
content: content, case 'tab1': // Post
tags: [], eventTemplate = {
created_at: Math.floor(Date.now() / 1000) kind: 1,
}; content: content,
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
break;
case 'tab2': // Reply
eventTemplate = {
kind: 1,
content: content,
tags: replyTags,
created_at: Math.floor(Date.now() / 1000)
};
break;
case 'tab3': // Create Profile
eventTemplate = {
kind: 0,
content: JSON.stringify({
name: name,
about: about,
picture: profilePic,
display_name: displayName,
website: website,
banner: banner,
nip05: nip05,
lud16: lud16
}),
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
break;
case 'tab4': // Create Profile
eventTemplate = {
kind: kind,
content: content,
tags: tags,
created_at: Math.floor(Date.now() / 1000)
};
break;
}
// Your existing event publishing logic here
console.log('Event to publish:', eventTemplate);
// ... rest of your publishing code
// Sign the event using window.nostr (NIP-07) // Sign the event using window.nostr (NIP-07)
finalEvent = await window.nostr.signEvent(eventTemplate); finalEvent = await window.nostr.signEvent(eventTemplate);
@@ -1928,8 +2242,11 @@
bounceCounter = 0; bounceCounter = 0;
// Clear UI elements // Clear UI elements
document.getElementById('final-content').value = ''; document.getElementById('post-content').value = '';
document.getElementById('final-event-display').textContent = ''; document.getElementById('final-event-display').textContent = '';
document.getElementById('fetched-event-display').textContent = '';
document.getElementById('fetched-event-display').style.display = 'none';
document.getElementById('fetched-event-label').style.display = 'none';
document.getElementById('bounces-container').innerHTML = ''; document.getElementById('bounces-container').innerHTML = '';
// Hide the Add Bounce button since no final event exists // Hide the Add Bounce button since no final event exists