diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2026-01-23 18:21:46 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-23 18:21:46 +0100 |
| commit | 322a17dba0d44200f6c241b18a58761220050aba (patch) | |
| tree | beaa47d7b7c5fe30e9dea95798d31d8320f508ae | |
| parent | 24b6948f0a1d63a28932b5306fa65bd09f668a97 (diff) | |
| download | m2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.tar.gz m2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.tar.xz m2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.zip | |
Update print statement from 'Hello' to 'Goodbye'
| -rw-r--r-- | index | 1648 |
1 files changed, 1648 insertions, 0 deletions
@@ -0,0 +1,1648 @@ +<?php +// /var/www/m2usenet/index.php +// m2usenet v2.6.0 - Face Header + Identity Hash v2 + +if (session_status() === PHP_SESSION_NONE) { + ini_set('session.cookie_httponly', 1); + ini_set('session.cookie_secure', 0); // .onion non ha TLS + ini_set('session.use_strict_mode', 1); + ini_set('session.cookie_samesite', 'Lax'); // Strict può bloccare su .onion + session_start(); +} + +if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} +$csrfToken = $_SESSION['csrf_token']; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>m2usenet Gateway v2.6</title> + <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; + } + + .dark-theme #saveIdentityWarning { + background: linear-gradient(135deg, #3d3510 0%, #4a4012 100%); + border-color: #856404; + } + + .dark-theme #saveIdentityWarning p { + color: #ffc107; + } + + 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="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js"></script> +</head> +<body> +<div class="container"> + <header> + <h1>m2usenet Gateway v2.6</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 m2usenet:</strong> This application is a privacy-focused gateway that sends your messages to Usenet newsgroups via Tor hidden services. No access logs are collected.</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" required></label> + <label>Email (for identity hash): <input type="email" id="hcEmail" placeholder="your@email.onion" required></label> + <label>Difficulty (bits): + <select id="hcBits"> + <option value="16">16 bits (very fast, ~instant - recommended for Tor Browser)</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;"><small>Hash: <code id="identityHashDisplay"></code></small></p> + </div> + </div> + <div class="identicon-actions" style="margin-top: 15px;"> + <button type="button" id="downloadIdenticonBtn" class="btn-secondary">📥 Identicon</button> + <button type="button" id="saveIdentityBtn" class="btn-secondary">💾 Save Identity</button> + </div> + <div id="saveIdentityWarning" style="margin-top: 15px; padding: 12px; background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%); border: 1px solid #ffc107; border-radius: 6px; text-align: center;"> + <p style="margin: 0; color: #856404; font-weight: bold;">⚠️ You must save your identity to continue!</p> + <p style="margin: 5px 0 0 0; color: #856404; font-size: 0.9em;">Click "Save Identity" above to download your identity file. This is required before signing and sending messages.</p> + </div> + <p style="margin-top: 15px; text-align: center; font-size: 0.9em;"> + <a href="/verify/" 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 message will be signed with Ed25519. The signature proves you authored this post.</p> + <p><strong>Face Header:</strong> Your identicon is embedded as a <code>Face:</code> header (RFC 4021) visible in newsreaders like Newsgrouper.</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..." 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 the Usenet network via Tor hidden services.</p> + <div class="gateway-info"> + <h4>🔐 Full Onion Network Path</h4> + <p style="margin-bottom: 15px; font-size: 0.95em;">Your message travels entirely within the Tor network:</p> + + <div class="gateway-item"> + <span class="gateway-priority">Step 1</span> + <div style="flex: 1;"> + <strong>SMTP Relays:</strong> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Specialized SMTP relays operating over Tor hidden services</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>PRIMARY:</strong> 4uwpi53u524xdphjw2dv5kywsxmyjxtk4facb76jgl3sc3nda3sz4fqd.onion:25</li> + <li><strong>FALLBACK:</strong> xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion:25</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@xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion</div> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Converts email to Usenet posts while maintaining full anonymity.</p> + </div> + </div> + + <div class="gateway-item"> + <span class="gateway-priority">Step 3</span> + <div style="flex: 1;"> + <strong>NNTP Server</strong> + <div class="gateway-address" style="margin-top: 5px;">nntp://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</div> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Final destination for Usenet Tor hidden service.</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> Your message never leaves the Tor network until it reaches the NNTP server. All three components (SMTP → Mail2News → NNTP) operate as Tor hidden services, providing end-to-end anonymity. + </p> + </div> + </div> + + <label>Email used for PoW:</label> + <input type="text" id="readonlyEmailSend" readonly> + <form id="sendForm" method="POST" action="send.php"> + <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($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" required placeholder="e.g. alt.privacy, comp.security"> + </label> + <label>Subject: <input type="text" name="subject" id="subject" required placeholder="Message Subject"></label> + <label>References (optional): <input type="text" name="references" id="references" 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" 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>m2usenet Gateway v2.6.0 © 2025 - Privacy-focused Usenet posting via Tor</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) { + // Block access to send tab if identity not saved + if (id === 'send' && !appState.identitySaved) { + showNotification('⚠ Please save your identity first before sending!', 'warning'); + return; + } + 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, + identitySaved: false, + prefillMode: null +}; + +function updateTabIndicators() { + if (appState.step1Complete) document.getElementById('tabBtn1').classList.add('completed'); + if (appState.step2Complete) document.getElementById('tabBtn2').classList.add('completed'); + if (appState.step3Complete) document.getElementById('tabBtn3').classList.add('completed'); +} + +let keyPair = null; +let workersSupported = true; + +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); + localStorage.setItem('darkTheme', this.checked); +}); + +// ============================================================================ +// IDENTICON GENERATION (client-side) +// ============================================================================ + +let currentIdenticonData = null; + +async function generateIdenticon(username, email, pubkeyBase64) { + const identityString = username + '|' + email + '|' + pubkeyBase64; + const encoder = new TextEncoder(); + const data = encoder.encode(identityString); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hash = new Uint8Array(hashBuffer); + const hashHex = Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join(''); + + const size = 128; + const gridSize = 5; + const padding = 0.08; + + const hue = ((hash[0] << 8) | hash[1]) % 360; + const fgColor = `hsl(${hue}, 65%, 45%)`; + const bgHue = (hue + 180) % 360; + const bgColor = `hsl(${bgHue}, 15%, 92%)`; + + const grid = []; + const halfWidth = Math.ceil(gridSize / 2); + let bitIndex = 16; + + for (let row = 0; row < gridSize; row++) { + grid[row] = []; + for (let col = 0; col < halfWidth; col++) { + const byteIndex = Math.floor(bitIndex / 8); + const bitOffset = bitIndex % 8; + const bit = (hash[byteIndex] >> bitOffset) & 1; + grid[row][col] = bit === 1; + bitIndex++; + } + for (let col = halfWidth; col < gridSize; col++) { + grid[row][col] = grid[row][gridSize - 1 - col]; + } + } + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + const paddingPx = Math.floor(size * padding); + const cellSize = Math.floor((size - paddingPx * 2) / gridSize); + const offset = paddingPx + Math.floor((size - paddingPx * 2 - cellSize * gridSize) / 2); + + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, size, size); + + ctx.fillStyle = fgColor; + for (let row = 0; row < gridSize; row++) { + for (let col = 0; col < gridSize; col++) { + if (grid[row][col]) { + ctx.fillRect(offset + col * cellSize, offset + row * cellSize, cellSize, cellSize); + } + } + } + + const dataUrl = canvas.toDataURL('image/png'); + + currentIdenticonData = { + dataUrl: dataUrl, + hash: hashHex, + username: username, + email: email, + pubkey: pubkeyBase64, + claim: JSON.stringify({ + version: 2, + type: 'm2usenet-identity', + username: username, + email: email, + pubkey: pubkeyBase64, + identityHash: hashHex, + created: new Date().toISOString(), + algorithm: 'sha256(username|email|pubkey)' + }, null, 2) + }; + + return dataUrl; +} + +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@m2usenet.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.substring(0, 16) + '...'; + document.getElementById('identityUsername').textContent = username; + document.getElementById('identityEmail').textContent = email; + document.getElementById('identiconSection').style.display = 'block'; + + } catch (e) { + console.error('Identicon generation failed:', e); + } +} + +document.getElementById('downloadIdenticonBtn').onclick = function() { + if (!currentIdenticonData) { + showNotification('⚠ Generate a keypair first!', 'warning'); + return; + } + + const a = document.createElement('a'); + a.href = currentIdenticonData.dataUrl; + a.download = 'identicon-' + currentIdenticonData.hash.substring(0, 8) + '.png'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + showNotification('✓ Identicon downloaded!', 'success', 3000); +}; + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + const darkTheme = localStorage.getItem('darkTheme') === 'true'; + document.getElementById('themeToggle').checked = darkTheme; + document.body.classList.toggle('dark-theme', darkTheme); + + 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('m2usenet Gateway v2.6.0 ready', 'success', 3000); +}); + +// ============================================================================ +// FORM SUBMISSION PROTECTION +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('form'); + const submitButton = form.querySelector('button[type="submit"]'); + + if (!submitButton) return; + + let isSubmitting = false; + + form.addEventListener('submit', function(e) { + if (isSubmitting) { + e.preventDefault(); + return false; + } + + isSubmitting = true; + submitButton.disabled = true; + submitButton.textContent = 'Sending...'; + submitButton.style.opacity = '0.6'; + submitButton.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 60 seconds via Tor. Please wait.'; + submitButton.parentNode.insertBefore(progressDiv, submitButton.nextSibling); + + return true; + }); +}); + +// ============================================================================ +// 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('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 || 'New keypair - save identity!'; + + await updateIdenticonPreview(); + + showNotification('✓ Key pair generated! Write your message and sign it.', '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').disabled = true; + document.getElementById('genKeyBtn').textContent = '✓ Keypair Loaded'; + + // Identity already saved (loaded from file) - allow access to send tab + appState.identitySaved = true; + document.getElementById('saveIdentityBtn').textContent = '✓ Identity Loaded'; + document.getElementById('saveIdentityBtn').style.background = '#28a745'; + + // Hide the warning message + const warning = document.getElementById('saveIdentityWarning'); + if (warning) warning.style.display = 'none'; + + showNotification('✓ Identity loaded! Username and email populated. Now generate PoW token.', 'success', 6000); + } catch (error) { + showNotification('✗ Failed to load identity: ' + error.message, 'error'); + } + + e.target.value = ''; +}; + +document.getElementById('saveIdentityBtn').onclick = async function() { + if (!keyPair) { + showNotification('⚠ Generate a keypair first!', 'warning'); + return; + } + + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || 'anonymous@m2usenet.local'; + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); + const secB64 = nacl.util.encodeBase64(keyPair.secretKey); + + const identityString = username + '|' + email + '|' + pubB64; + const encoder = new TextEncoder(); + const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(identityString)); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const identityHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + const data = { + version: 2, + type: 'm2usenet-identity', + username: username, + email: email, + publicKey: pubB64, + secretKey: secB64, + identityHash: identityHash, + algorithm: 'sha256(username|email|pubkey)', + created: new Date().toISOString(), + warning: 'Keep this file secure! Anyone with this file can sign messages as you.' + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'm2usenet-identity-' + username + '.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // Mark identity as saved - allows access to send tab + appState.identitySaved = true; + + // Update button to show saved state + this.textContent = '✓ Identity Saved'; + this.style.background = '#28a745'; + + // Hide the warning message + const warning = document.getElementById('saveIdentityWarning'); + if (warning) warning.style.display = 'none'; + + // If message already signed, go to send tab + if (appState.step2Complete) { + showNotification('✓ Identity saved! Proceeding to send...', 'success', 4000); + setTimeout(() => showTab('send'), 1500); + } else { + showNotification('✓ Identity saved! Now sign your message to continue.', 'success', 6000); + } +}; + +document.getElementById('signMsgBtn').onclick = function() { + try { + if (!keyPair) { + showNotification('⚠ Please generate or load a key pair first!', 'warning'); + return; + } + + const msg = 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(); + + // After signing, check if identity is saved + if (!appState.identitySaved) { + // Scroll to identicon section and prompt to save + showNotification('✓ Message signed! Now save your identity to continue.', 'warning', 10000); + setTimeout(() => { + const identiconSection = document.getElementById('identiconSection'); + if (identiconSection) { + identiconSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + identiconSection.style.animation = 'pulse 1s ease-in-out 3'; + } + }, 500); + } else { + // Identity already saved, proceed to send + showNotification('✓ Message signed with your identity!', 'success', 8000); + setTimeout(() => showTab('send'), 2000); + } + } 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', function(e) { + 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) { + e.preventDefault(); + showNotification(`⚠ Missing: ${missing.join(', ')}`, 'warning', 8000); + return false; + } + + const messageLen = document.getElementById('messageContent').value.length; + if (messageLen < 10) { + e.preventDefault(); + showNotification('⚠ Message too short', 'warning'); + return false; + } + + const newsgroups = document.getElementById('newsgroups').value.split(',').map(g => g.trim()).filter(g => g); + if (newsgroups.length === 0) { + e.preventDefault(); + showNotification('⚠ Enter at least one newsgroup!', 'warning'); + return false; + } + + if (newsgroups.length > 3) { + e.preventDefault(); + showNotification('⚠ Max 3 newsgroups!', 'warning'); + return false; + } + + const confirmMsg = `Send message?\n\nNewsgroups: ${newsgroups.join(', ')}\nSubject: ${document.getElementById('subject').value}\nLength: ${messageLen} chars\n\nDelivery via Tor may take up to 60 seconds.`; + + if (!confirm(confirmMsg)) { + e.preventDefault(); + return false; + } + + showNotification('📤 Sending message...', 'info', 0); + document.getElementById('sendBtn').disabled = true; + document.getElementById('sendBtn').textContent = 'Sending...'; + + return true; +}); + +// ============================================================================ +// 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; + + try { + const storedData = localStorage.getItem('m2usenet_prefill'); + if (storedData) { + prefillData = JSON.parse(storedData); + prefillSource = 'localStorage'; + localStorage.removeItem('m2usenet_prefill'); + + if (prefillData.newsgroups) prefillData.newsgroups = decode(prefillData.newsgroups); + if (prefillData.subject) prefillData.subject = decode(prefillData.subject); + if (prefillData.references) prefillData.references = decode(prefillData.references); + } + } catch (e) { + localStorage.removeItem('m2usenet_prefill'); + } + + if (!prefillData) { + 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> |
