diff options
Diffstat (limited to 'internal/assets/web/templates/index.html')
| -rw-r--r-- | internal/assets/web/templates/index.html | 1577 |
1 files changed, 1577 insertions, 0 deletions
diff --git a/internal/assets/web/templates/index.html b/internal/assets/web/templates/index.html new file mode 100644 index 0000000..0d2d820 --- /dev/null +++ b/internal/assets/web/templates/index.html @@ -0,0 +1,1577 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>N2Usenet Gateway v2.7 Nym</title> + <link rel="icon" href="/favicon.ico" sizes="any"> + <link rel="shortcut icon" href="/favicon.ico"> + <style> + :root { + --background: #f9f9f9; + --card-bg: #fff; + --text: #333; + --border: #ccc; + --primary: #4caf50; + --primary-hover: #45a049; + --tab-bg: #eee; + --input-bg: #fff; + --input-readonly: #eee; + --progress-bg: #ddd; + --success: #28a745; + --warning: #ffc107; + --error: #dc3545; + } + + .dark-theme { + --background: #1a1a1a; + --card-bg: #2c2c2c; + --text: #e0e0e0; + --border: #444; + --primary: #5cbb60; + --primary-hover: #4caf50; + --tab-bg: #333; + --input-bg: #3c3c3c; + --input-readonly: #2a2a2a; + --progress-bg: #444; + } + + body { + background: var(--background); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + margin: 0; + padding: 0; + transition: background 0.3s ease; + } + + .container { + max-width: 800px; + margin: auto; + padding: 20px; + } + + header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .theme-toggle { + display: flex; + align-items: center; + gap: 8px; + } + + .toggle { + position: relative; + width: 60px; + height: 30px; + background: #ccc; + border-radius: 30px; + padding: 4px; + cursor: pointer; + transition: 0.3s; + } + + .toggle:before { + content: ''; + position: absolute; + width: 26px; + height: 26px; + border-radius: 50%; + background: white; + top: 2px; + left: 2px; + transition: 0.3s; + } + + input[type="checkbox"]:checked + .toggle { + background: #4caf50; + } + + input[type="checkbox"]:checked + .toggle:before { + transform: translateX(30px); + } + + input[type="checkbox"] { + display: none; + } + + .notification { + position: fixed; + top: 20px; + right: 20px; + max-width: 400px; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 10000; + animation: slideIn 0.3s ease; + display: flex; + align-items: center; + gap: 10px; + } + + .notification.success { background: var(--success); color: white; } + .notification.error { background: var(--error); color: white; } + .notification.warning { background: var(--warning); color: #333; } + .notification.info { background: #007bff; color: white; } + + @keyframes slideIn { + from { transform: translateX(400px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + + @keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0.7); } + 50% { box-shadow: 0 0 0 15px rgba(156, 39, 176, 0); } + 100% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0); } + } + + .notification-icon { font-size: 1.5em; } + .notification-close { margin-left: auto; cursor: pointer; font-size: 1.2em; opacity: 0.8; } + .notification-close:hover { opacity: 1; } + + .section-info { + background: var(--card-bg); + border-left: 4px solid var(--primary); + padding: 10px 15px; + margin-bottom: 15px; + border-radius: 0 4px 4px 0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + .tabs { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 15px; + } + + .tabs button { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 12px 15px; + border: none; + background: var(--tab-bg); + color: var(--text); + cursor: pointer; + border-radius: 4px; + font-weight: bold; + transition: background 0.2s, transform 0.1s; + position: relative; + } + + .tabs button:hover { background: var(--primary); color: white; } + .tabs button:active { transform: scale(0.98); } + + .tabs button.completed::after { + content: '✓'; + position: absolute; + right: 15px; + background: var(--success); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9em; + } + + .tab-content { + display: none !important; + padding: 20px; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + margin-bottom: 20px; + } + + .tab-content.active { + display: block !important; + } + + label { + display: block; + margin-top: 15px; + font-weight: bold; + margin-bottom: 5px; + } + + input, textarea, select { + width: 100%; + padding: 10px; + margin-top: 5px; + box-sizing: border-box; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--input-bg); + color: var(--text); + font-family: inherit; + } + + input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); + } + + input.error, textarea.error { border-color: var(--error); } + + input[readonly], textarea[readonly] { + background: var(--input-readonly); + cursor: not-allowed; + border: 2px solid #999; + } + + input.prefilled { + background: #e8f5e9; + border: 2px solid var(--primary); + } + + .dark-theme input.prefilled { + background: #1b3d1b; + } + + button { + margin-top: 15px; + padding: 12px 20px; + background: var(--primary); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + transition: background 0.2s, opacity 0.2s; + font-family: inherit; + } + + button:hover:not(:disabled) { background: var(--primary-hover); } + button:disabled { opacity: 0.6; cursor: not-allowed; } + + .btn-secondary { + background: #6c757d; + } + .btn-secondary:hover:not(:disabled) { background: #5a6268; } + + .progress-bar { + width: 100%; + background: var(--progress-bg); + border-radius: 5px; + overflow: hidden; + height: 20px; + margin-top: 10px; + } + + .progress-bar-inner { + height: 100%; + width: 0; + background: var(--primary); + text-align: center; + color: white; + line-height: 20px; + transition: width 0.3s; + font-size: 0.85em; + } + + .output-field { + margin-top: 15px; + padding: 10px; + background: var(--input-readonly); + border: 1px solid var(--border); + border-radius: 4px; + word-break: break-all; + min-height: 30px; + font-family: monospace; + font-size: 0.9em; + } + + .output-field.empty { color: #999; font-style: italic; } + + .gateway-info { + background: var(--card-bg); + border: 1px solid var(--border); + padding: 15px; + margin: 15px 0; + border-radius: 4px; + border-left: 4px solid #007bff; + } + + .gateway-item { + display: flex; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--border); + } + + .gateway-item:last-child { border-bottom: none; } + + .gateway-priority { + background: #007bff; + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-right: 10px; + min-width: 70px; + text-align: center; + } + + .gateway-address { + font-family: monospace; + background: var(--input-readonly); + padding: 4px 8px; + border-radius: 4px; + flex: 1; + font-size: 0.85em; + } + + .prefill-banner { + background: linear-gradient(135deg, #4caf50 0%, #45a049 100%); + color: white; + padding: 12px 15px; + border-radius: 4px; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); + } + + .prefill-banner.reply { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + } + + .prefill-banner-icon { font-size: 1.5em; } + .prefill-banner-text { flex: 1; } + .prefill-banner-text strong { display: block; margin-bottom: 2px; } + .prefill-banner-text small { opacity: 0.9; } + + .keypair-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 15px; + } + + .keypair-actions button { + flex: 1; + min-width: 150px; + } + + .identity-loaded { + background: #d4edda; + border: 1px solid #c3e6cb; + padding: 10px 15px; + border-radius: 4px; + margin-top: 15px; + display: flex; + align-items: center; + gap: 10px; + } + + .dark-theme .identity-loaded { + background: #1e3d1e; + border-color: #2e5d2e; + } + + .identity-loaded-icon { font-size: 1.5em; } + .identity-loaded-text { flex: 1; } + + .identicon-preview { + display: inline-block; + vertical-align: middle; + margin-left: 10px; + } + + .identicon-preview img { + width: 32px; + height: 32px; + image-rendering: pixelated; + border: 1px solid var(--border); + border-radius: 4px; + } + + .identicon-section { + background: var(--card-bg); + border: 1px solid var(--border); + border-left: 4px solid #9c27b0; + padding: 15px; + margin: 15px 0; + border-radius: 0 4px 4px 0; + text-align: center; + } + + .identicon-section h4 { + margin: 0 0 15px 0; + color: #9c27b0; + } + + .identicon-large-preview { + margin: 15px 0; + } + + .identicon-hash { + font-size: 0.8em; + color: #666; + word-break: break-all; + margin: 10px 0; + } + + .identicon-hash code { + background: var(--input-readonly); + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + max-width: 100%; + overflow-x: auto; + } + + .identicon-actions { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; + margin: 15px 0; + } + + .identicon-actions button { + margin: 0; + padding: 8px 15px; + font-size: 0.9em; + } + + .identicon-tip { + background: #f3e5f5; + padding: 10px; + border-radius: 4px; + margin-top: 10px; + } + + .dark-theme .identicon-tip { + background: #2d1f30; + } + + footer { + text-align: center; + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border); + font-size: 0.9em; + color: #888; + } + + .footer-links { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 10px; + } + + .footer-links a { + color: var(--primary); + text-decoration: none; + transition: color 0.2s; + } + + .footer-links a:hover { + color: var(--primary-hover); + text-decoration: underline; + } + + @media (max-width: 600px) { + .container { padding: 10px; } + .tab-content { padding: 15px 10px; } + .footer-links { flex-direction: column; gap: 10px; } + .gateway-item { flex-direction: column; align-items: flex-start; gap: 5px; } + .gateway-address { width: 100%; } + .notification { right: 10px; left: 10px; max-width: none; } + .keypair-actions { flex-direction: column; } + .keypair-actions button { min-width: 100%; } + } + </style> + <script src="/static/nacl.min.js"></script> + <script src="/static/nacl-util.min.js"></script> +</head> +<body> +<div class="container"> + <header> + <h1>N2Usenet Gateway v2.7 Nym</h1> + <div class="theme-toggle"> + <span>🌞</span> + <input type="checkbox" id="themeToggle"> + <label for="themeToggle" class="toggle"></label> + <span>🌙</span> + </div> + </header> + + <div id="prefillBanner" class="prefill-banner" style="display: none;"> + <span class="prefill-banner-icon">📝</span> + <div class="prefill-banner-text"> + <strong id="prefillTitle">New Post</strong> + <small id="prefillDetails">Fields pre-filled from Onion Newsreader</small> + </div> + </div> + + <div class="tabs"> + <button id="tabBtn1" onclick="showTab('pow')"> + <span>1. Generate Hashcash Token</span> + </button> + <button id="tabBtn2" onclick="showTab('sign')"> + <span>2. Sign Message (Identity)</span> + </button> + <button id="tabBtn3" onclick="showTab('send')"> + <span>3. Send Message</span> + </button> + </div> + + <div id="pow" class="tab-content active"> + <h2>1. Load Identity or Start Fresh</h2> + + <div class="identity-loader" style="background: var(--card-bg); border: 2px dashed var(--primary); border-radius: 8px; padding: 20px; margin-bottom: 20px; text-align: center;"> + <p style="margin: 0 0 15px 0;"><strong>🔑 Have a saved identity?</strong> Load it to auto-fill all fields.</p> + <button id="loadIdentityBtn" class="btn-primary" style="font-size: 1.1em; padding: 12px 30px;">📂 Load Identity File</button> + <input type="file" id="identityFileInput" accept=".json" style="display:none;"> + <p style="margin: 15px 0 0 0; font-size: 0.9em; color: var(--text); opacity: 0.7;">Or fill the fields below to create a new identity.</p> + </div> + + <div id="identityLoadedPow" class="identity-loaded" style="display:none; margin-bottom: 20px;"> + <span class="identity-loaded-icon">✅</span> + <div class="identity-loaded-text"> + <strong id="loadedIdentityName">Identity Loaded</strong> + <small id="loadedIdentityEmail">email@example.com</small> + </div> + <div class="identicon-preview"> + <img id="identiconPreviewPow" alt="Your identicon" style="width: 48px; height: 48px; image-rendering: pixelated; border-radius: 4px;"> + </div> + </div> + + <div class="section-info"> + <p><strong>About N2Usenet:</strong> A privacy-focused standalone gateway that posts to Usenet newsgroups over Nym to the TCPReset Mail2News and NNTP infrastructure. No access logs are kept. To post you must first create, or load, a <strong>VFACE pseudonymous identity</strong>: an Ed25519 keypair whose public key deterministically generates your identicon. Same key → same identicon → same person over time.</p> + <p><strong>What is Proof-of-Work?</strong> This step generates a hashcash token that prevents spam by requiring your computer to perform some calculations.</p> + </div> + + <label>Username (your pseudonym): <input type="text" id="fromName" placeholder="YourPseudonym" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></label> + <label>Email (for identity hash): <input type="email" id="hcEmail" placeholder="your@email.example" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></label> + <label>Difficulty (bits): + <select id="hcBits"> + <option value="16">16 bits (very fast, ~instant - recommended for hardened browsers)</option> + <option value="20" selected>20 bits (fast, ~few seconds)</option> + <option value="24">24 bits (medium, ~30-60 seconds)</option> + <option value="28">28 bits (slow, ~several minutes)</option> + </select> + </label> + <button id="genTokenBtn">Generate Token</button> + <div class="progress-bar"><div id="tokenProgress" class="progress-bar-inner">Ready</div></div> + <label>Generated Token:</label> + <div id="tokenOutput" class="output-field empty">Token will appear here after generation</div> + </div> + + <div id="sign" class="tab-content"> + <h2>2. Sign Your Message</h2> + + <div id="identiconSection" class="identicon-section" style="display:none;"> + <h4>🎨 Your Visual Identity</h4> + <div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;"> + <div class="identicon-large-preview"> + <img id="identiconPreviewLarge" alt="Your identicon" style="width:96px;height:96px;image-rendering:pixelated;border:2px solid var(--border);border-radius:8px;"> + </div> + <div style="flex: 1; min-width: 200px;"> + <p style="margin: 0 0 5px 0;"><strong id="identityUsername">Username</strong></p> + <p style="margin: 0 0 10px 0; font-size: 0.9em; color: var(--text); opacity: 0.8;" id="identityEmail">email@example.com</p> + <p class="identicon-hash" style="margin: 0;">Hash: <code id="identityHashDisplay"></code></p> + </div> + </div> + <p style="margin-top: 15px; text-align: center; font-size: 0.95em;">Identity material stays only in browser memory for this tab and is wiped after a successful send.</p> + <p style="margin-top: 15px; text-align: center; font-size: 0.9em;"> + <a href="https://identicons.virebent.art" target="_blank" rel="noopener noreferrer" style="color: #9c27b0; text-decoration: none;">🔐 VFACE Verifier - Verify an identity</a> + </p> + </div> + + <div class="section-info"> + <p><strong>What is this?</strong> Your keypair is generated and your message is signed <strong>locally in your browser</strong> with the self-hosted TweetNaCl (<code>nacl</code>) library, and your Ed25519 secret key never leaves your device. Only the public identity (username, email, public key) is sent, to render the identicon. The signature proves you authored this post.</p> + <p><strong>VFACE / Face Header:</strong> Your identicon is produced by the original <code>identicons-cli</code> engine, the same backend as <code>identicons.virebent.art</code>, so it is fully deterministic: the same key always yields the same identicon. It is embedded as a folded <code>Face:</code> header (RFC 4021/2822) alongside <code>X-Ed25519-Pub</code> and <code>X-Ed25519-Sig</code>, visible in newsreaders like Newsgrouper and re-verifiable by any reader from your public key.</p> + </div> + + <label>Email (from PoW):</label> + <input type="text" id="readonlyEmailSign" readonly> + + <label>Message to Sign:</label> + <textarea id="messageToSign" rows="6" placeholder="Write your message here..." autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></textarea> + + <div class="keypair-actions"> + <button id="genKeyBtn">🔑 Generate New Keypair</button> + </div> + <input type="file" id="keyFileInput" accept=".json" style="display:none;"> + + <button id="signMsgBtn" disabled>✍️ Sign Message</button> + + <label>Public Key:</label> + <div id="pubKeyOutput" class="output-field empty">Public key will appear here</div> + <label>Signature:</label> + <div id="signatureOutput" class="output-field empty">Signature will appear here</div> + </div> + + <div id="send" class="tab-content"> + <h2>Send Message</h2> + <div class="section-info"> + <p><strong>What is this?</strong> This final step sends your signed message to Usenet via Nym, Mail2News, and the <code>news.tcpreset.net</code> NNTP service.</p> + <div class="gateway-info"> + <h4>🔐 Nym to TCPReset Posting Path</h4> + <p style="margin-bottom: 15px; font-size: 0.95em;">Your message is submitted over HTTPS, then relayed from this server through Nym to the Mail2News gateway and finally into <code>news.tcpreset.net</code> NNTP:</p> + + <div class="gateway-item"> + <span class="gateway-priority">Step 1</span> + <div style="flex: 1;"> + <strong>Nym SMTP transport</strong> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Nym SOCKS5/requester transport to the TCPReset Mail2News SMTP gateway.</p> + </div> + </div> + <details style="margin-top: 8px; font-size: 0.8em;"> + <summary style="cursor: pointer; color: #007bff;">Show relay nodes</summary> + <ul style="margin: 8px 0; padding-left: 20px; font-family: monospace; color: #555;"> + <li><strong>SMTP:</strong> mail2news.tcpreset.net:587 STARTTLS via Nym SOCKS5</li> + </ul> + </details> + + <div class="gateway-item"> + <span class="gateway-priority">Step 2</span> + <div style="flex: 1;"> + <strong>Mail2News Gateway</strong> + <div class="gateway-address" style="margin-top: 5px;">mail2news@mail2news.tcpreset.net</div> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Converts the signed email payload into a Usenet post for the TCPReset news service.</p> + </div> + </div> + + <div class="gateway-item"> + <span class="gateway-priority">Step 3</span> + <div style="flex: 1;"> + <strong>TCPReset NNTP</strong> + <div class="gateway-address" style="margin-top: 5px;">nntp://news.tcpreset.net</div> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Final posting target for the standalone N2Usenet path.</p> + </div> + </div> + + <p style="margin-top: 15px; padding: 10px; background: rgba(0,123,255,0.1); border-radius: 4px; font-size: 0.9em;"> + <strong>🔒 Privacy Guarantee:</strong> The browser-to-server hop is HTTPS. The server-to-Mail2News hop uses Nym plus SMTP TLS. Mail2News then posts into <code>news.tcpreset.net</code> NNTP. + </p> + </div> + </div> + + <label>Email used for PoW:</label> + <input type="text" id="readonlyEmailSend" readonly> + <form id="sendForm" method="POST" action="/submit" autocomplete="off"> + <input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> + <label>From (Name):</label> + <input type="text" id="readonlyFromName" readonly> + <input type="hidden" name="from" id="fromFull"> + <label>Newsgroups (max 3, comma separated): + <input type="text" name="newsgroups" id="newsgroups" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="e.g. alt.privacy, comp.security"> + </label> + <label>Subject: <input type="text" name="subject" id="subject" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="Message Subject"></label> + <label>References (optional): <input type="text" name="references" id="references" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Message-ID of post you're replying to"></label> + <label>X-Hashcash Token: <input type="text" name="xhashcash" id="hcToken" required readonly></label> + <label>Message:</label> + <textarea name="message" id="messageContent" rows="8" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="Your message will appear here after signing..."></textarea> + <input type="hidden" name="x-ed25519-pub" id="x-ed25519-pub"> + <input type="hidden" name="x-ed25519-sig" id="x-ed25519-sig"> + <button type="submit" id="sendBtn">Send Message</button> + </form> + </div> + + <footer> + <div>N2Usenet Gateway v2.7.0 © 2025 - Privacy-focused Usenet posting via Nym</div> + <div class="footer-links"> + <a href="https://yamn.virebent.art">Home</a> + <a href="mailto:%69%6E%66%6F%40%76%69%72%65%62%65%6E%74%2E%61%72%74">Contact</a> + <a href="https://github.com/gabrix73/m2usenet-go">Code</a> + </div> + </footer> +</div> + +<script> +// ============================================================================ +// GLOBAL FUNCTIONS - MUST BE DEFINED FIRST +// ============================================================================ + +function showTab(id) { + document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); + document.getElementById(id).classList.add('active'); +} + +function showNotification(message, type, duration) { + type = type || 'info'; + duration = duration === undefined ? 5000 : duration; + const icons = { success: '✓', error: '✗', warning: '⚠', info: 'ℹ' }; + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.innerHTML = ` + <span class="notification-icon">${icons[type] || icons.info}</span> + <span class="notification-message">${message}</span> + <span class="notification-close" onclick="this.parentElement.remove()">×</span> + `; + document.body.appendChild(notification); + if (duration > 0) { + setTimeout(() => { if (notification.parentElement) notification.remove(); }, duration); + } +} + +// ============================================================================ +// STATE MANAGEMENT +// ============================================================================ + +const appState = { + step1Complete: false, + step2Complete: false, + step3Complete: false, + prefillMode: null +}; + +function updateTabIndicators() { + document.getElementById('tabBtn1').classList.toggle('completed', appState.step1Complete); + document.getElementById('tabBtn2').classList.toggle('completed', appState.step2Complete); + document.getElementById('tabBtn3').classList.toggle('completed', appState.step3Complete); +} + +let keyPair = null; +let workersSupported = true; + +function setOutputField(id, value, emptyText) { + const el = document.getElementById(id); + if (!el) return; + if (value) { + el.innerText = value; + el.classList.remove('empty'); + return; + } + el.innerText = emptyText; + el.classList.add('empty'); +} + +function resetEphemeralState(messageID) { + if (keyPair) { + if (keyPair.secretKey && typeof keyPair.secretKey.fill === 'function') keyPair.secretKey.fill(0); + if (keyPair.publicKey && typeof keyPair.publicKey.fill === 'function') keyPair.publicKey.fill(0); + } + keyPair = null; + currentIdenticonData = null; + + appState.step1Complete = false; + appState.step2Complete = false; + appState.step3Complete = false; + updateTabIndicators(); + + [ + 'fromName', 'hcEmail', 'readonlyEmailSign', 'readonlyEmailSend', + 'readonlyFromName', 'fromFull', 'newsgroups', 'subject', 'references', + 'hcToken', 'messageToSign', 'messageContent', 'x-ed25519-pub', + 'x-ed25519-sig' + ].forEach((id) => { + const el = document.getElementById(id); + if (el) el.value = ''; + }); + + ['identityFileInput', 'keyFileInput'].forEach((id) => { + const el = document.getElementById(id); + if (el) el.value = ''; + }); + + document.querySelectorAll('input, textarea').forEach((el) => { + el.classList.remove('error', 'prefilled'); + }); + + const hcBits = document.getElementById('hcBits'); + if (hcBits) hcBits.value = '20'; + + setOutputField('tokenOutput', '', 'Token will appear here after generation'); + setOutputField('pubKeyOutput', '', 'Public key will appear here'); + setOutputField('signatureOutput', '', 'Signature will appear here'); + + const tokenProgress = document.getElementById('tokenProgress'); + if (tokenProgress) { + tokenProgress.style.width = '0'; + tokenProgress.textContent = 'Ready'; + } + + const identityLoaded = document.getElementById('identityLoadedPow'); + if (identityLoaded) identityLoaded.style.display = 'none'; + const identiconSection = document.getElementById('identiconSection'); + if (identiconSection) identiconSection.style.display = 'none'; + const prefillBanner = document.getElementById('prefillBanner'); + if (prefillBanner) { + prefillBanner.style.display = 'none'; + prefillBanner.classList.remove('reply'); + } + + ['identiconPreviewPow', 'identiconPreviewLarge'].forEach((id) => { + const img = document.getElementById(id); + if (img) img.removeAttribute('src'); + }); + const identityHash = document.getElementById('identityHashDisplay'); + if (identityHash) identityHash.textContent = ''; + const loadedName = document.getElementById('loadedIdentityName'); + if (loadedName) loadedName.textContent = 'Identity Loaded'; + const loadedEmail = document.getElementById('loadedIdentityEmail'); + if (loadedEmail) loadedEmail.textContent = 'Identity kept only in memory'; + const identityUsername = document.getElementById('identityUsername'); + if (identityUsername) identityUsername.textContent = 'Username'; + const identityEmail = document.getElementById('identityEmail'); + if (identityEmail) identityEmail.textContent = 'email@example.com'; + + const genKeyBtn = document.getElementById('genKeyBtn'); + if (genKeyBtn) { + genKeyBtn.disabled = false; + genKeyBtn.textContent = '🔑 Generate New Keypair'; + } + const signBtn = document.getElementById('signMsgBtn'); + if (signBtn) signBtn.disabled = true; + const sendBtn = document.getElementById('sendBtn'); + if (sendBtn) { + sendBtn.disabled = false; + sendBtn.textContent = 'Send Message'; + sendBtn.style.opacity = ''; + sendBtn.style.cursor = ''; + } + const progressDiv = document.getElementById('sending-progress'); + if (progressDiv) progressDiv.remove(); + + if (window.history && window.history.replaceState) { + window.history.replaceState({}, document.title, window.location.pathname); + } + + showTab('pow'); + + if (messageID) { + showNotification(`✓ Message sent. Message-ID: ${messageID}. Identity and form data cleared from memory.`, 'success', 12000); + } +} + +try { + if (typeof Worker === 'undefined') { + workersSupported = false; + } +} catch (e) { + workersSupported = false; +} + +// ============================================================================ +// THEME TOGGLE +// ============================================================================ + +document.getElementById('themeToggle').addEventListener('change', function() { + document.body.classList.toggle('dark-theme', this.checked); +}); + +// ============================================================================ +// IDENTICON GENERATION (client-side) +// ============================================================================ + +let currentIdenticonData = null; + +async function generateIdenticon(username, email, pubkeyBase64) { + // Generate the identicon with the ORIGINAL identicons-cli engine (server-side), + // the same backend used by identicons.virebent.art (Ch1ffr3punk algorithm). + // Only PUBLIC data leaves the browser (username|email|ed25519-pubkey). + const body = new URLSearchParams({ username: username, email: email, pubkey: pubkeyBase64 }); + const resp = await fetch('identicon.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString() + }); + if (!resp.ok) throw new Error('identicon engine error ' + resp.status); + const r = await resp.json(); + if (!r.face48 || !r.preview) throw new Error('identicon engine returned no data'); + + currentIdenticonData = { + dataUrl: r.preview, // 256x256 PNG dataurl (display/download) + hash: r.hash, // sha256(username|email|pubkey) + face48: r.face48, // raw base64 PNG (48x48) -> Face: header + faceHeader: r.faceHeader, // folded "Face: ..." header (RFC 2822) + username: username, + email: email, + pubkey: pubkeyBase64, + claim: JSON.stringify({ + version: 2, + type: 'n2usenet-identity', + username: username, + email: email, + pubkey: pubkeyBase64, + identityHash: r.hash, + created: new Date().toISOString(), + algorithm: 'identicons-cli sha256(username|email|pubkey) [VFACE]' + }, null, 2) + }; + return r.preview; +} + +async function updateIdenticonPreview() { + if (!keyPair) return; + + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || 'anonymous@n2usenet.local'; + + try { + const identiconUrl = await generateIdenticon(username, email, pubB64); + + document.getElementById('identiconPreviewPow').src = identiconUrl; + document.getElementById('identiconPreviewLarge').src = identiconUrl; + document.getElementById('identityHashDisplay').textContent = currentIdenticonData.hash; + document.getElementById('identityUsername').textContent = username; + document.getElementById('identityEmail').textContent = email; + document.getElementById('identiconSection').style.display = 'block'; + + } catch (e) { + console.error('Identicon generation failed:', e); + } +} + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + if (typeof nacl === 'undefined') { + showNotification('Cryptography library failed to load. Please refresh the page.', 'error', 0); + return; + } + + if (typeof nacl.util === 'undefined') { + nacl.util = { + decodeUTF8: function(str) { return new TextEncoder().encode(str); }, + encodeUTF8: function(arr) { return new TextDecoder().decode(arr); }, + encodeBase64: function(arr) { + return btoa(Array.from(new Uint8Array(arr)).map(byte => String.fromCharCode(byte)).join('')); + }, + decodeBase64: function(b64) { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return arr; + } + }; + } + + showTab('pow'); + showNotification('N2Usenet Gateway v2.7.0 ready. No identity or message data is persisted.', 'success', 4000); +}); + +// ============================================================================ +// SHA-1 IMPLEMENTATION +// ============================================================================ + +function sha1(str) { + function rotate_left(n, s) { return (n << s) | (n >>> (32 - s)); } + function cvt_hex(val) { + let str = ''; + for (let i = 7; i >= 0; i--) { + const v = (val >>> (i * 4)) & 0x0f; + str += v.toString(16); + } + return str; + } + function utf8Encode(str) { return unescape(encodeURIComponent(str)); } + + let blockstart, i, j; + const W = new Array(80); + let H0 = 0x67452301, H1 = 0xEFCDAB89, H2 = 0x98BADCFE, H3 = 0x10325476, H4 = 0xC3D2E1F0; + let A, B, C, D, E, temp; + + str = utf8Encode(str); + const str_len = str.length; + const word_array = []; + + for (i = 0; i < str_len - 3; i += 4) { + j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3); + word_array.push(j); + } + + switch (str_len % 4) { + case 0: i = 0x080000000; break; + case 1: i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; break; + case 2: i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; break; + case 3: i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; break; + } + + word_array.push(i); + while ((word_array.length % 16) != 14) word_array.push(0); + word_array.push(str_len >>> 29); + word_array.push((str_len << 3) & 0x0ffffffff); + + for (blockstart = 0; blockstart < word_array.length; blockstart += 16) { + for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i]; + for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); + + A = H0; B = H1; C = H2; D = H3; E = H4; + + for (i = 0; i <= 19; i++) { + temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; + E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; + } + for (i = 20; i <= 39; i++) { + temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; + E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; + } + for (i = 40; i <= 59; i++) { + temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; + E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; + } + for (i = 60; i <= 79; i++) { + temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; + E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; + } + + H0 = (H0 + A) & 0x0ffffffff; + H1 = (H1 + B) & 0x0ffffffff; + H2 = (H2 + C) & 0x0ffffffff; + H3 = (H3 + D) & 0x0ffffffff; + H4 = (H4 + E) & 0x0ffffffff; + } + + return cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); +} + +function mineSingleThread(prefix, targetZeros, progressCallback, foundCallback) { + const target = '0'.repeat(targetZeros); + let nonce = 0, checked = 0; + const batchSize = 100; + + function mineNextBatch() { + const endNonce = nonce + batchSize; + while (nonce < endNonce) { + const token = prefix + nonce; + const hash = sha1(token); + checked++; + if (hash.startsWith(target)) { + foundCallback(nonce, checked); + return; + } + nonce++; + } + progressCallback(checked); + setTimeout(mineNextBatch, 0); + } + mineNextBatch(); +} + +// ============================================================================ +// STEP 1: HASHCASH TOKEN GENERATION +// ============================================================================ + +document.getElementById('genTokenBtn').onclick = () => { + const email = document.getElementById('hcEmail').value.trim(); + const bits = parseInt(document.getElementById('hcBits').value); + + if (!email) { + showNotification('⚠ Please enter your email address first!', 'warning'); + document.getElementById('hcEmail').classList.add('error'); + document.getElementById('hcEmail').focus(); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + showNotification('⚠ Please enter a valid email address!', 'warning'); + document.getElementById('hcEmail').classList.add('error'); + document.getElementById('hcEmail').focus(); + return; + } + + document.getElementById('hcEmail').classList.remove('error'); + showNotification(`🔨 Starting token generation (${bits} bits)...`, 'info'); + + const btn = document.getElementById('genTokenBtn'); + btn.disabled = true; + btn.textContent = 'Mining Token...'; + + const now = new Date(); + const timestamp = now.getUTCFullYear().toString().slice(2) + + ("0"+(now.getUTCMonth()+1)).slice(-2) + + ("0"+now.getUTCDate()).slice(-2) + + ("0"+now.getUTCHours()).slice(-2) + + ("0"+now.getUTCMinutes()).slice(-2) + + ("0"+now.getUTCSeconds()).slice(-2); + const ext = ""; + const rand = Math.floor(Math.random()*1e6); + const prefix = `1:${bits}:${timestamp}:${email}:${ext}:${rand}:`; + const zeros = bits/4; + + const progBar = document.getElementById('tokenProgress'); + progBar.style.width = '0%'; + progBar.innerText = 'Mining...'; + + let totalChecked = 0; + let startTime = Date.now(); + + function updateProgress(checked) { + totalChecked += checked; + const elapsed = (Date.now() - startTime) / 1000; + const hashRate = Math.round(totalChecked / elapsed); + progBar.style.width = Math.min(95, (totalChecked / 10000)) + '%'; + progBar.innerText = `${totalChecked} hashes (${hashRate}/s)`; + } + + function onFound(nonce, totalHashes) { + const token = prefix + nonce; + const outputField = document.getElementById('tokenOutput'); + outputField.innerText = token; + outputField.classList.remove('empty'); + document.getElementById('hcToken').value = token; + + const fromName = document.getElementById('fromName').value || 'Anonymous'; + document.getElementById('fromFull').value = `${fromName} <${email}>`; + document.getElementById('readonlyFromName').value = fromName; + document.getElementById('readonlyEmailSign').value = email; + document.getElementById('readonlyEmailSend').value = email; + + btn.disabled = false; + btn.textContent = 'Generate Token'; + progBar.style.width = '100%'; + progBar.innerText = `Complete! (${totalHashes} hashes)`; + + appState.step1Complete = true; + updateTabIndicators(); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + showNotification(`✓ Token generated in ${elapsed}s!`, 'success', 8000); + + setTimeout(() => showTab('sign'), 2000); + } + + if (workersSupported) { + try { + const cores = navigator.hardwareConcurrency || 2; + let found = false; + let coreChecked = Array(cores).fill(0); + const workers = []; + + function updateCombinedProgress() { + const sum = coreChecked.reduce((a,b)=>a+b,0); + updateProgress(sum - totalChecked); + } + + for (let i = 0; i < cores; i++) { + try { + const w = new Worker('/static/powWorker.js'); + w.postMessage({ prefix, targetZeros: zeros, startNonce: i, step: cores }); + + w.onmessage = e => { + if (found) return; + if (e.data.type === 'found') { + found = true; + workers.forEach(x => x.terminate()); + onFound(e.data.nonce, e.data.checked); + } + if (e.data.type === 'progress') { + coreChecked[i] = e.data.checked; + updateCombinedProgress(); + } + }; + + w.onerror = (err) => { + if (i === 0) { + workers.forEach(x => x.terminate()); + mineSingleThread(prefix, zeros, updateProgress, onFound); + } + }; + + workers.push(w); + } catch (err) { + if (i === 0) { + mineSingleThread(prefix, zeros, updateProgress, onFound); + return; + } + } + } + + } catch (err) { + mineSingleThread(prefix, zeros, updateProgress, onFound); + } + } else { + mineSingleThread(prefix, zeros, updateProgress, onFound); + } +}; + +// ============================================================================ +// STEP 2: ED25519 SIGNATURE + KEYPAIR MANAGEMENT +// ============================================================================ + +document.getElementById('genKeyBtn').onclick = async function() { + try { + showNotification('🔐 Generating Ed25519 key pair...', 'info'); + keyPair = nacl.sign.keyPair(); + + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); + + const pubOutput = document.getElementById('pubKeyOutput'); + pubOutput.innerText = pubB64; + pubOutput.classList.remove('empty'); + document.getElementById('x-ed25519-pub').value = pubB64; + document.getElementById('signMsgBtn').disabled = false; + + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || ''; + document.getElementById('identityLoadedPow').style.display = 'flex'; + document.getElementById('loadedIdentityName').textContent = username; + document.getElementById('loadedIdentityEmail').textContent = email || 'Identity active only in memory'; + + await updateIdenticonPreview(); + showNotification('✓ Identity generated in memory. Now write and sign your message.', 'success', 6000); + } catch (error) { + showNotification('✗ Error: ' + error.message, 'error'); + } +}; + +document.getElementById('loadIdentityBtn').onclick = function() { + document.getElementById('identityFileInput').click(); +}; + +document.getElementById('identityFileInput').onchange = async function(e) { + const file = e.target.files[0]; + if (!file) return; + + try { + const text = await file.text(); + const data = JSON.parse(text); + + if (!data.publicKey || !data.secretKey) { + throw new Error('Invalid identity file format'); + } + + const pubKey = nacl.util.decodeBase64(data.publicKey); + const secKey = nacl.util.decodeBase64(data.secretKey); + + if (pubKey.length !== 32 || secKey.length !== 64) { + throw new Error('Invalid key lengths'); + } + + keyPair = { publicKey: pubKey, secretKey: secKey }; + + const pubB64 = data.publicKey; + const pubOutput = document.getElementById('pubKeyOutput'); + pubOutput.innerText = pubB64; + pubOutput.classList.remove('empty'); + document.getElementById('x-ed25519-pub').value = pubB64; + document.getElementById('signMsgBtn').disabled = false; + + if (data.username) { + document.getElementById('fromName').value = data.username; + document.getElementById('readonlyFromName').value = data.username; + } + if (data.email) { + document.getElementById('hcEmail').value = data.email; + const fromName = document.getElementById('fromName').value || 'Anonymous'; + document.getElementById('fromFull').value = `${fromName} <${data.email}>`; + document.getElementById('readonlyEmailSign').value = data.email; + document.getElementById('readonlyEmailSend').value = data.email; + } + + const username = data.username || 'Anonymous'; + const email = data.email || ''; + document.getElementById('identityLoadedPow').style.display = 'flex'; + document.getElementById('loadedIdentityName').textContent = username; + document.getElementById('loadedIdentityEmail').textContent = email; + + await updateIdenticonPreview(); + + if (currentIdenticonData && currentIdenticonData.dataUrl) { + document.getElementById('identiconPreviewPow').src = currentIdenticonData.dataUrl; + } + + document.getElementById('genKeyBtn').textContent = '🔑 Replace Keypair'; + showNotification('✓ Identity loaded into memory only. Now generate PoW token.', 'success', 6000); + } catch (error) { + showNotification('✗ Failed to load identity: ' + error.message, 'error'); + } + + e.target.value = ''; +}; + +// Usenet line-wrap convention (RFC 1855 Netiquette Guidelines): wrap plain-text +// body lines at ~72 columns for compatibility with old-style newsreaders. +// Must run BEFORE signing, not after, so the Ed25519 signature covers the +// exact bytes that end up posted to Usenet. +function wrapUsenetText(text, width) { + width = width || 72; + return text.split('\n').map(function(line) { + if (line.length === 0 || line.startsWith('>')) return line; + const words = line.split(' '); + const wrapped = []; + let current = ''; + for (const word of words) { + if (current === '') { + current = word; + } else if ((current + ' ' + word).length <= width) { + current += ' ' + word; + } else { + wrapped.push(current); + current = word; + } + } + wrapped.push(current); + return wrapped.join('\n'); + }).join('\n'); +} + +document.getElementById('signMsgBtn').onclick = function() { + try { + if (!keyPair) { + showNotification('⚠ Please generate or load a key pair first!', 'warning'); + return; + } + + const msg = wrapUsenetText(document.getElementById('messageToSign').value.trim()); + if (!msg) { + showNotification('⚠ Please write a message!', 'warning'); + document.getElementById('messageToSign').classList.add('error'); + document.getElementById('messageToSign').focus(); + return; + } + + if (msg.length < 10) { + showNotification('⚠ Message too short (min 10 characters)', 'warning'); + return; + } + + document.getElementById('messageToSign').classList.remove('error'); + showNotification('✍ Signing message...', 'info'); + + const msgBytes = nacl.util.decodeUTF8(msg); + const sig = nacl.sign.detached(msgBytes, keyPair.secretKey); + const sigB64 = nacl.util.encodeBase64(sig); + + const sigOutput = document.getElementById('signatureOutput'); + sigOutput.innerText = sigB64; + sigOutput.classList.remove('empty'); + document.getElementById('x-ed25519-sig').value = sigB64; + + document.getElementById('messageContent').value = msg + "\n\n--- Digital Signature ---\n" + sigB64; + + appState.step2Complete = true; + updateTabIndicators(); + showNotification('✓ Message signed with your in-memory identity.', 'success', 8000); + setTimeout(() => showTab('send'), 1200); + } catch (error) { + showNotification('✗ Error: ' + error.message, 'error'); + } +}; + +document.getElementById('fromName').addEventListener('input', function() { + const email = document.getElementById('hcEmail').value; + document.getElementById('readonlyFromName').value = this.value; + if (email) { + document.getElementById('fromFull').value = `${this.value} <${email}>`; + } + if (keyPair) { + updateIdenticonPreview(); + } +}); + +// ============================================================================ +// STEP 3: SEND FORM +// ============================================================================ + +document.getElementById('sendForm').addEventListener('submit', async function(e) { + e.preventDefault(); + + const requiredFields = [ + {id: 'fromFull', name: 'From'}, + {id: 'newsgroups', name: 'Newsgroups'}, + {id: 'subject', name: 'Subject'}, + {id: 'hcToken', name: 'Hashcash Token'}, + {id: 'messageContent', name: 'Message'}, + {id: 'x-ed25519-pub', name: 'Public Key'}, + {id: 'x-ed25519-sig', name: 'Signature'} + ]; + + const missing = []; + requiredFields.forEach(field => { + const el = document.getElementById(field.id); + if (!el.value.trim()) { + missing.push(field.name); + el.classList.add('error'); + } else { + el.classList.remove('error'); + } + }); + + if (missing.length > 0) { + showNotification(`⚠ Missing: ${missing.join(', ')}`, 'warning', 8000); + return; + } + + const messageLen = document.getElementById('messageContent').value.length; + if (messageLen < 10) { + showNotification('⚠ Message too short', 'warning'); + return; + } + + const newsgroups = document.getElementById('newsgroups').value.split(',').map(g => g.trim()).filter(g => g); + if (newsgroups.length === 0) { + showNotification('⚠ Enter at least one newsgroup!', 'warning'); + return; + } + + if (newsgroups.length > 3) { + showNotification('⚠ Max 3 newsgroups!', 'warning'); + return; + } + + const confirmMsg = `Send message?\n\nNewsgroups: ${newsgroups.join(', ')}\nSubject: ${document.getElementById('subject').value}\nLength: ${messageLen} chars\n\nDelivery via Nym may take up to 120 seconds.`; + + if (!confirm(confirmMsg)) { + return; + } + + showNotification('📤 Sending message...', 'info', 0); + const sendBtn = document.getElementById('sendBtn'); + sendBtn.disabled = true; + sendBtn.textContent = 'Sending...'; + sendBtn.style.opacity = '0.6'; + sendBtn.style.cursor = 'not-allowed'; + + const progressDiv = document.createElement('div'); + progressDiv.id = 'sending-progress'; + progressDiv.style.cssText = 'margin-top: 10px; padding: 10px; background: #fff3cd; border-left: 4px solid #ffc107; color: #856404;'; + progressDiv.innerHTML = '<strong>⏳ Sending message...</strong><br>This may take up to 120 seconds via Nym. Please wait.'; + sendBtn.parentNode.insertBefore(progressDiv, sendBtn.nextSibling); + + try { + const resp = await fetch('/submit', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: new URLSearchParams(new FormData(this)).toString(), + credentials: 'same-origin', + cache: 'no-store' + }); + let data = {}; + try { + data = await resp.json(); + } catch (err) {} + if (!resp.ok) { + throw new Error(data.error || 'Delivery failed.'); + } + appState.step3Complete = true; + updateTabIndicators(); + resetEphemeralState(data.messageId || ''); + } catch (error) { + if (progressDiv.parentNode) progressDiv.remove(); + sendBtn.disabled = false; + sendBtn.textContent = 'Send Message'; + sendBtn.style.opacity = ''; + sendBtn.style.cursor = ''; + showNotification('✗ ' + error.message, 'error', 10000); + } +}); + +// ============================================================================ +// ONION-NEWSREADER INTEGRATION +// ============================================================================ + +(function() { + function decode(s) { + if (!s) return ''; + try { + return decodeURIComponent(s.replace(/\+/g, ' ')); + } catch(e) { + return s; + } + } + + function fillField(id, value, highlight) { + highlight = highlight !== false; + const field = document.getElementById(id); + if (field && value) { + field.value = value; + if (highlight) field.classList.add('prefilled'); + return true; + } + return false; + } + + function showPrefillBanner(isReply, details) { + const banner = document.getElementById('prefillBanner'); + const title = document.getElementById('prefillTitle'); + const detailsEl = document.getElementById('prefillDetails'); + + if (banner) { + banner.style.display = 'flex'; + if (isReply) { + banner.classList.add('reply'); + title.textContent = '💬 Reply Mode'; + } else { + title.textContent = '📝 New Post'; + } + detailsEl.textContent = details; + } + } + + let prefillData = null; + let prefillSource = null; + const params = new URLSearchParams(window.location.search); + const action = params.get('action'); + + if (action === 'new' || action === 'reply' || params.has('newsgroups')) { + prefillData = { + newsgroups: decode(params.get('newsgroups') || ''), + subject: decode(params.get('subject') || ''), + references: decode(params.get('references') || ''), + body: decode(params.get('body') || '') + }; + prefillSource = 'URL'; + } + + if (prefillData) { + let filledFields = []; + + if (prefillData.newsgroups) { + fillField('newsgroups', prefillData.newsgroups); + filledFields.push('Newsgroups'); + } + + if (prefillData.subject) { + fillField('subject', prefillData.subject); + filledFields.push('Subject'); + } + + if (prefillData.references) { + fillField('references', prefillData.references); + filledFields.push('References'); + } + + if (prefillData.body) { + fillField('messageToSign', prefillData.body); + filledFields.push('Quoted message'); + } + + const isReply = !!(prefillData.references || + (prefillData.subject && prefillData.subject.toLowerCase().startsWith('re:'))); + + if (filledFields.length > 0) { + const details = `Pre-filled: ${filledFields.join(', ')} (from ${prefillSource})`; + showPrefillBanner(isReply, details); + + if (isReply) { + showNotification(`💬 Reply mode: ${prefillData.newsgroups}`, 'info', 5000); + } else { + showNotification(`📝 New post to: ${prefillData.newsgroups}`, 'success', 5000); + } + } + + setTimeout(() => showTab('pow'), 300); + } +})(); +</script> + +</body> +</html> |
