|
|
|
|
@ -100,15 +100,56 @@
@@ -100,15 +100,56 @@
|
|
|
|
|
</div> |
|
|
|
|
</form> |
|
|
|
|
|
|
|
|
|
<!-- Contact Submission Modal --> |
|
|
|
|
<div id="contact-modal" class="modal" style="display: none;"> |
|
|
|
|
<div class="modal-content"> |
|
|
|
|
<div class="modal-header"> |
|
|
|
|
<h2>Choose Submission Method</h2> |
|
|
|
|
<button type="button" class="modal-close" id="modal-close-btn" aria-label="Close">×</button> |
|
|
|
|
</div> |
|
|
|
|
<div class="modal-body"> |
|
|
|
|
<p>How would you like to submit your message?</p> |
|
|
|
|
<div class="modal-options"> |
|
|
|
|
<button type="button" id="login-btn" class="btn btn-primary"> |
|
|
|
|
<span class="icon-inline">{{icon "key"}}</span> Login with Browser Extension |
|
|
|
|
</button> |
|
|
|
|
<button type="button" id="anonymous-btn" class="btn btn-secondary"> |
|
|
|
|
<span class="icon-inline">{{icon "user-x"}}</span> Submit Anonymously |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{{if .RepoAnnouncement}} |
|
|
|
|
<script type="application/json" id="repo-announcement-data">{{json .RepoAnnouncement}}</script> |
|
|
|
|
{{end}} |
|
|
|
|
|
|
|
|
|
<script type="application/json" id="contact-relays-data">{{json .ContactRelays}}</script> |
|
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@1.18.0/lib/nostr.bundle.js"></script> |
|
|
|
|
<script> |
|
|
|
|
(function() { |
|
|
|
|
const form = document.getElementById('contact-form'); |
|
|
|
|
const submitBtn = document.getElementById('submit-btn'); |
|
|
|
|
const statusDiv = document.getElementById('nostr-status'); |
|
|
|
|
const modal = document.getElementById('contact-modal'); |
|
|
|
|
const modalCloseBtn = document.getElementById('modal-close-btn'); |
|
|
|
|
const loginBtn = document.getElementById('login-btn'); |
|
|
|
|
const anonymousBtn = document.getElementById('anonymous-btn'); |
|
|
|
|
|
|
|
|
|
// Get contact relays from JSON script tag |
|
|
|
|
let contactRelays = []; |
|
|
|
|
const contactRelaysEl = document.getElementById('contact-relays-data'); |
|
|
|
|
if (contactRelaysEl) { |
|
|
|
|
try { |
|
|
|
|
contactRelays = JSON.parse(contactRelaysEl.textContent); |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error('Failed to parse contact relays data:', e); |
|
|
|
|
// Fallback to empty array |
|
|
|
|
contactRelays = []; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get repo announcement data from JSON script tag |
|
|
|
|
let repoAnnouncement = null; |
|
|
|
|
@ -121,6 +162,9 @@
@@ -121,6 +162,9 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Store form data for submission |
|
|
|
|
let pendingFormData = null; |
|
|
|
|
|
|
|
|
|
function showStatus(message, isError) { |
|
|
|
|
statusDiv.textContent = message; |
|
|
|
|
statusDiv.className = 'alert ' + (isError ? 'alert-error' : 'alert-success'); |
|
|
|
|
@ -131,37 +175,90 @@
@@ -131,37 +175,90 @@
|
|
|
|
|
statusDiv.style.display = 'none'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
form.addEventListener('submit', async function(e) { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
|
|
// Check if Nostr extension is available |
|
|
|
|
if (!window.nostr) { |
|
|
|
|
showStatus('Nostr extension not found. Please install a Nostr browser extension (e.g., nos2x, Alby) to submit issues.', true); |
|
|
|
|
return; |
|
|
|
|
function showModal() { |
|
|
|
|
modal.style.display = 'flex'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function hideModal() { |
|
|
|
|
modal.style.display = 'none'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Close modal handlers |
|
|
|
|
modalCloseBtn.addEventListener('click', hideModal); |
|
|
|
|
modal.addEventListener('click', function(e) { |
|
|
|
|
if (e.target === modal) { |
|
|
|
|
hideModal(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Generate key pair for anonymous submission |
|
|
|
|
async function generateKeyPair() { |
|
|
|
|
const keyPair = NostrTools.generatePrivateKey(); |
|
|
|
|
const pubkey = NostrTools.getPublicKey(keyPair); |
|
|
|
|
return { privateKey: keyPair, pubkey: pubkey }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Sign event with private key |
|
|
|
|
function signEvent(event, privateKey) { |
|
|
|
|
return NostrTools.finalizeEvent(event, privateKey); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Submit event |
|
|
|
|
async function submitEvent(signedEvent) { |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Publishing...'; |
|
|
|
|
|
|
|
|
|
if (!repoAnnouncement) { |
|
|
|
|
showStatus('Repository configuration not available. Please try again later.', true); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const response = await fetch('/api/contact', { |
|
|
|
|
method: 'POST', |
|
|
|
|
headers: { |
|
|
|
|
'Content-Type': 'application/json', |
|
|
|
|
}, |
|
|
|
|
body: JSON.stringify({ event: signedEvent }) |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const subject = document.getElementById('subject').value.trim(); |
|
|
|
|
const content = document.getElementById('content').value.trim(); |
|
|
|
|
const labelsStr = document.getElementById('labels').value.trim(); |
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
|
|
|
|
if (!subject || !content) { |
|
|
|
|
showStatus('Subject and message are required.', true); |
|
|
|
|
return; |
|
|
|
|
if (response.ok && result.success) { |
|
|
|
|
window.location.href = '/contact?success=true&event_id=' + result.event_id; |
|
|
|
|
} else { |
|
|
|
|
showStatus('Failed to publish: ' + (result.error || 'Unknown error'), true); |
|
|
|
|
submitBtn.disabled = false; |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Disable submit button |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Process submission with pubkey |
|
|
|
|
async function processSubmission(useExtension) { |
|
|
|
|
hideModal(); |
|
|
|
|
submitBtn.disabled = true; |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Signing...'; |
|
|
|
|
hideStatus(); |
|
|
|
|
|
|
|
|
|
const { subject, content, labelsStr } = pendingFormData; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Get user's public key |
|
|
|
|
const pubkey = await window.nostr.getPublicKey(); |
|
|
|
|
let pubkey; |
|
|
|
|
let signFunction; |
|
|
|
|
|
|
|
|
|
if (useExtension) { |
|
|
|
|
// Login with browser extension |
|
|
|
|
if (!window.nostr) { |
|
|
|
|
showStatus('Nostr extension not found. Please install a Nostr browser extension (e.g., nos2x, Alby).', true); |
|
|
|
|
submitBtn.disabled = false; |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit'; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Signing...'; |
|
|
|
|
pubkey = await window.nostr.getPublicKey(); |
|
|
|
|
signFunction = (event) => window.nostr.signEvent(event); |
|
|
|
|
} else { |
|
|
|
|
// Anonymous submission - generate key pair |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Generating key...'; |
|
|
|
|
const keyPair = await generateKeyPair(); |
|
|
|
|
pubkey = keyPair.pubkey; |
|
|
|
|
signFunction = (event) => { |
|
|
|
|
return signEvent(event, keyPair.privateKey); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse labels |
|
|
|
|
const labels = labelsStr ? labelsStr.split(',').map(l => l.trim()).filter(l => l) : []; |
|
|
|
|
@ -169,18 +266,24 @@
@@ -169,18 +266,24 @@
|
|
|
|
|
// Build event tags |
|
|
|
|
const tags = []; |
|
|
|
|
|
|
|
|
|
// Add 'a' tag for repository announcement |
|
|
|
|
tags.push(['a', `30617:${repoAnnouncement.pubkey}:${repoAnnouncement.dTag}`]); |
|
|
|
|
// Add 'a' tag for repository announcement if available |
|
|
|
|
if (repoAnnouncement) { |
|
|
|
|
tags.push(['a', `30617:${repoAnnouncement.pubkey}:${repoAnnouncement.dTag}`]); |
|
|
|
|
tags.push(['p', repoAnnouncement.pubkey]); |
|
|
|
|
|
|
|
|
|
if (repoAnnouncement.maintainers && repoAnnouncement.maintainers.length > 0) { |
|
|
|
|
repoAnnouncement.maintainers.forEach(maintainer => { |
|
|
|
|
tags.push(['p', maintainer]); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add 'p' tag for repository owner |
|
|
|
|
tags.push(['p', repoAnnouncement.pubkey]); |
|
|
|
|
// Add required 'p' tags for contact recipients |
|
|
|
|
tags.push(['p', '846ebf79a0a8813274ec9727490621ad423f16a3e474d7fd66e6a98bfe4e39a4']); |
|
|
|
|
tags.push(['p', 'fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1']); |
|
|
|
|
|
|
|
|
|
// Add maintainers as 'p' tags |
|
|
|
|
if (repoAnnouncement.maintainers && repoAnnouncement.maintainers.length > 0) { |
|
|
|
|
repoAnnouncement.maintainers.forEach(maintainer => { |
|
|
|
|
tags.push(['p', maintainer]); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
// Add client tag |
|
|
|
|
tags.push(['client', 'gitcitadel.com']); |
|
|
|
|
|
|
|
|
|
// Add subject tag |
|
|
|
|
if (subject) { |
|
|
|
|
@ -194,49 +297,61 @@
@@ -194,49 +297,61 @@
|
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Add relays tag if available |
|
|
|
|
if (repoAnnouncement.relays && repoAnnouncement.relays.length > 0) { |
|
|
|
|
tags.push(['relays', ...repoAnnouncement.relays]); |
|
|
|
|
} |
|
|
|
|
// Add contact relays tag |
|
|
|
|
tags.push(['relays', ...contactRelays]); |
|
|
|
|
|
|
|
|
|
// Create unsigned event |
|
|
|
|
// Create unsigned event (kind 1 for contact messages) |
|
|
|
|
const unsignedEvent = { |
|
|
|
|
kind: 1621, |
|
|
|
|
kind: 1, |
|
|
|
|
pubkey: pubkey, |
|
|
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
|
|
tags: tags, |
|
|
|
|
content: content |
|
|
|
|
content: `Subject: ${subject}\n\n${content}` |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// Sign the event |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Publishing...'; |
|
|
|
|
const signedEvent = await window.nostr.signEvent(unsignedEvent); |
|
|
|
|
|
|
|
|
|
// Send to API |
|
|
|
|
const response = await fetch('/api/contact', { |
|
|
|
|
method: 'POST', |
|
|
|
|
headers: { |
|
|
|
|
'Content-Type': 'application/json', |
|
|
|
|
}, |
|
|
|
|
body: JSON.stringify({ event: signedEvent }) |
|
|
|
|
}); |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Signing...'; |
|
|
|
|
const signedEvent = await signFunction(unsignedEvent); |
|
|
|
|
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
|
|
|
|
if (response.ok && result.success) { |
|
|
|
|
// Redirect to success page |
|
|
|
|
window.location.href = '/contact?success=true&event_id=' + result.event_id; |
|
|
|
|
} else { |
|
|
|
|
showStatus('Failed to publish issue: ' + (result.error || 'Unknown error'), true); |
|
|
|
|
submitBtn.disabled = false; |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.854 2.147-10.94 10.939"/></svg></span> Submit'; |
|
|
|
|
} |
|
|
|
|
// Submit event |
|
|
|
|
await submitEvent(signedEvent); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error:', error); |
|
|
|
|
showStatus('Error: ' + error.message, true); |
|
|
|
|
submitBtn.disabled = false; |
|
|
|
|
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Login button handler |
|
|
|
|
loginBtn.addEventListener('click', () => processSubmission(true)); |
|
|
|
|
|
|
|
|
|
// Anonymous button handler |
|
|
|
|
anonymousBtn.addEventListener('click', () => processSubmission(false)); |
|
|
|
|
|
|
|
|
|
// Form submit handler |
|
|
|
|
form.addEventListener('submit', async function(e) { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
|
|
if (!repoAnnouncement) { |
|
|
|
|
showStatus('Repository configuration not available. Please try again later.', true); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const subject = document.getElementById('subject').value.trim(); |
|
|
|
|
const content = document.getElementById('content').value.trim(); |
|
|
|
|
const labelsStr = document.getElementById('labels').value.trim(); |
|
|
|
|
|
|
|
|
|
if (!subject || !content) { |
|
|
|
|
showStatus('Subject and message are required.', true); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Store form data |
|
|
|
|
pendingFormData = { subject, content, labelsStr }; |
|
|
|
|
|
|
|
|
|
// Show modal |
|
|
|
|
showModal(); |
|
|
|
|
}); |
|
|
|
|
})(); |
|
|
|
|
</script> |
|
|
|
|
|