Browse Source

finished contact form with client and p-tags and browser extension log-in or anonymous submission

master
Silberengel 4 weeks ago
parent
commit
7ad6268f0b
  1. 14
      cmd/server/main.go
  2. 7
      config.yaml.example
  3. 61
      internal/config/config.go
  4. 3
      internal/generator/html.go
  5. 57
      internal/nostr/client.go
  6. 62
      internal/server/handlers.go
  7. BIN
      server
  8. 79
      static/css/main.css
  9. 24
      static/css/responsive.css
  10. 217
      templates/contact.html
  11. 2
      templates/feed.html

14
cmd/server/main.go

@ -51,7 +51,9 @@ func main() { @@ -51,7 +51,9 @@ func main() {
}
// Initialize Nostr client
nostrClient := nostr.NewClient(cfg.Relays.Primary, cfg.Relays.Fallback, cfg.Relays.AdditionalFallback)
profileRelays := cfg.GetProfilesRelays()
contactRelays := cfg.GetContactFormRelays()
nostrClient := nostr.NewClient(cfg.Relays.Feeds, profileRelays, contactRelays)
ctx := context.Background()
if err := nostrClient.Connect(ctx); err != nil {
logger.Warnf("Failed to connect to relays: %v", err)
@ -60,10 +62,15 @@ func main() { @@ -60,10 +62,15 @@ func main() {
// Initialize services
// Use standard Nostr kind constants
articleKinds := nostr.SupportedArticleKinds()
wikiService := nostr.NewWikiService(nostrClient, articleKinds, nostr.KindWiki, cfg.Relays.AdditionalFallback, nostr.KindIndex, nostr.KindBlog, nostr.KindLongform)
// Use first profile relay for wiki service (fallback)
wikiRelay := cfg.Relays.Feeds
if len(profileRelays) > 0 {
wikiRelay = profileRelays[0]
}
wikiService := nostr.NewWikiService(nostrClient, articleKinds, nostr.KindWiki, wikiRelay, nostr.KindIndex, nostr.KindBlog, nostr.KindLongform)
feedService := nostr.NewFeedService(nostrClient, nostr.KindNote)
issueService := nostr.NewIssueService(nostrClient, nostr.KindIssue, nostr.KindRepoAnnouncement)
ebooksService := nostr.NewEBooksService(nostrClient, nostr.KindIndex, "wss://theforest.nostr1.com")
ebooksService := nostr.NewEBooksService(nostrClient, nostr.KindIndex, cfg.Relays.Feeds)
// Initialize HTML generator
htmlGenerator, err := generator.NewHTMLGenerator(
@ -98,7 +105,6 @@ func main() { @@ -98,7 +105,6 @@ func main() {
logger.Info("Generating initial landing page...")
initialLandingHTML, err := htmlGenerator.GenerateLandingPage(
[]generator.WikiPageInfo{},
[]generator.FeedItemInfo{},
nil, // newestBlogItem
nil, // newestArticleItem
[]generator.ArticleItemInfo{}, // allArticleItems

7
config.yaml.example

@ -2,14 +2,13 @@ wiki_index: "naddr1qvzqqqr4tqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8aj @@ -2,14 +2,13 @@ wiki_index: "naddr1qvzqqqr4tqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8aj
blog_index: "naddr1qvzqqqr4tqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqyvhwumn8ghj7enjv4jkccte9eek7anzd96zu6r0wd6qzxmhwden5te0w35x2cmfw3skgetv9ehx7um5wgcjucm0d5q35amnwvaz7tm5dpjkvmmjv4ehgtnwdaehgu339e3k7mgpzpmhxue69uhkummnw3ezumrpdejqzyrhwden5te0dehhxarj9emkjmn9qythwumn8ghj7mn0wd68ytnndamxy6t59e5x7um5qyghwumn8ghj7mn0wd68yv339e3k7mgqy96xsefdva5hgcmfw3skgetv943xcmm89438jttnw3jkcmrp94mz6vggpn2pq"
repo_announcement: "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqq9xw6t5vd5hgctyv4kqde47kt"
relays:
primary: "wss://theforest.nostr1.com"
fallback: "wss://nostr.land"
additional_fallback: "wss://thecitadel.nostr1.com"
feeds: "wss://theforest.nostr1.com"
profiles: "wss://theforest.nostr1.com","wss://nostr.land","wss://thecitadel.nostr1.com"
contactform: "wss://thecitadel.nostr1.com","wss://relay.damus.io","wss://freelay.sovbit.host","wss://relay.primal.net",
link_base_url: "https://alexandria.gitcitadel.eu"
cache:
refresh_interval_minutes: 30
feed:
relay: "wss://theforest.nostr1.com"
poll_interval_minutes: 5
max_events: 30
server:

61
internal/config/config.go

@ -3,6 +3,7 @@ package config @@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
@ -13,9 +14,9 @@ type Config struct { @@ -13,9 +14,9 @@ type Config struct {
BlogIndex string `yaml:"blog_index"`
RepoAnnouncement string `yaml:"repo_announcement"` // naddr for kind 30617 repo announcement
Relays struct {
Primary string `yaml:"primary"`
Fallback string `yaml:"fallback"`
AdditionalFallback string `yaml:"additional_fallback"`
Feeds string `yaml:"feeds"` // Single relay for feeds
Profiles string `yaml:"profiles"` // Comma-separated list of relays for profiles and deletion events
ContactForm string `yaml:"contactform"` // Comma-separated list of relays for contact form
} `yaml:"relays"`
LinkBaseURL string `yaml:"link_base_url"`
Cache struct {
@ -50,14 +51,14 @@ func LoadConfig(path string) (*Config, error) { @@ -50,14 +51,14 @@ func LoadConfig(path string) (*Config, error) {
}
// Set defaults
if config.Relays.Primary == "" {
config.Relays.Primary = "wss://theforest.nostr1.com"
if config.Relays.Feeds == "" {
config.Relays.Feeds = "wss://theforest.nostr1.com"
}
if config.Relays.Fallback == "" {
config.Relays.Fallback = "wss://nostr.land"
if config.Relays.Profiles == "" {
config.Relays.Profiles = "wss://theforest.nostr1.com,wss://nostr.land,wss://thecitadel.nostr1.com"
}
if config.Relays.AdditionalFallback == "" {
config.Relays.AdditionalFallback = "wss://thecitadel.nostr1.com"
if config.Relays.ContactForm == "" {
config.Relays.ContactForm = "wss://thecitadel.nostr1.com,wss://relay.damus.io,wss://freelay.sovbit.host,wss://relay.primal.net"
}
if config.LinkBaseURL == "" {
config.LinkBaseURL = "https://alexandria.gitcitadel.eu"
@ -66,7 +67,7 @@ func LoadConfig(path string) (*Config, error) { @@ -66,7 +67,7 @@ func LoadConfig(path string) (*Config, error) {
config.Cache.RefreshIntervalMinutes = 30
}
if config.Feed.Relay == "" {
config.Feed.Relay = "wss://theforest.nostr1.com"
config.Feed.Relay = config.Relays.Feeds
}
if config.Feed.PollIntervalMinutes == 0 {
config.Feed.PollIntervalMinutes = 5
@ -95,13 +96,49 @@ func LoadConfig(path string) (*Config, error) { @@ -95,13 +96,49 @@ func LoadConfig(path string) (*Config, error) {
return &config, nil
}
// GetProfilesRelays parses the comma-separated profiles relay string into a slice
func (c *Config) GetProfilesRelays() []string {
if c.Relays.Profiles == "" {
return []string{}
}
relays := strings.Split(c.Relays.Profiles, ",")
result := make([]string, 0, len(relays))
for _, relay := range relays {
relay = strings.TrimSpace(relay)
// Remove quotes if present
relay = strings.Trim(relay, "\"'")
if relay != "" {
result = append(result, relay)
}
}
return result
}
// GetContactFormRelays parses the comma-separated contact form relay string into a slice
func (c *Config) GetContactFormRelays() []string {
if c.Relays.ContactForm == "" {
return []string{}
}
relays := strings.Split(c.Relays.ContactForm, ",")
result := make([]string, 0, len(relays))
for _, relay := range relays {
relay = strings.TrimSpace(relay)
// Remove quotes if present
relay = strings.Trim(relay, "\"'")
if relay != "" {
result = append(result, relay)
}
}
return result
}
// Validate validates the configuration
func (c *Config) Validate() error {
if c.WikiIndex == "" {
return fmt.Errorf("wiki_index is required")
}
if c.Relays.Primary == "" {
return fmt.Errorf("relays.primary is required")
if c.Relays.Feeds == "" {
return fmt.Errorf("relays.feeds is required")
}
return nil
}

3
internal/generator/html.go

@ -780,6 +780,9 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event @@ -780,6 +780,9 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
}
}
// Add contact relays for JavaScript
templateData["ContactRelays"] = g.nostrClient.GetContactRelays()
// Execute base.html which will use the blocks from contact.html
var buf bytes.Buffer
if err := renderTmpl.ExecuteTemplate(&buf, "base.html", templateData); err != nil {

57
internal/nostr/client.go

@ -15,34 +15,36 @@ import ( @@ -15,34 +15,36 @@ import (
type Client struct {
pool *nostr.SimplePool
relays []string
feedsRelay string // Relay for feeds
profileRelays []string // Relays for profiles
contactRelays []string // Relays for contact form
mu sync.RWMutex
ctx context.Context
requestSem chan struct{} // Semaphore to limit concurrent requests
maxConcurrent int // Maximum concurrent requests
}
// NewClient creates a new Nostr client with primary, fallback, and additional fallback relays
// NewClient creates a new Nostr client with relay configuration
// Uses go-nostr's SimplePool for connection management and parallel queries
// Limits concurrent requests to prevent overwhelming relays (default: 5 concurrent requests)
func NewClient(primaryRelay, fallbackRelay, additionalFallback string) *Client {
func NewClient(feedsRelay string, profileRelays []string, contactRelays []string) *Client {
ctx := context.Background()
pool := nostr.NewSimplePool(ctx)
// Build relay list: feeds relay first, then profile relays
relays := []string{}
if primaryRelay != "" {
relays = append(relays, primaryRelay)
}
if fallbackRelay != "" {
relays = append(relays, fallbackRelay)
}
if additionalFallback != "" {
relays = append(relays, additionalFallback)
if feedsRelay != "" {
relays = append(relays, feedsRelay)
}
relays = append(relays, profileRelays...)
maxConcurrent := 5 // Limit to 5 concurrent requests to avoid overwhelming relays
return &Client{
pool: pool,
relays: relays,
feedsRelay: feedsRelay,
profileRelays: profileRelays,
contactRelays: contactRelays,
ctx: ctx,
requestSem: make(chan struct{}, maxConcurrent),
maxConcurrent: maxConcurrent,
@ -223,23 +225,19 @@ func (c *Client) GetRelays() []string { @@ -223,23 +225,19 @@ func (c *Client) GetRelays() []string {
return c.relays
}
// GetPrimaryRelay returns the primary relay (theforest) for main event fetching
// GetPrimaryRelay returns the feeds relay for main event fetching
func (c *Client) GetPrimaryRelay() string {
if len(c.relays) > 0 {
return c.relays[0]
}
return ""
return c.feedsRelay
}
// GetProfileRelays returns fallback relays for profile fetching (excludes primary/theforest)
// GetProfileRelays returns relays for profile fetching
func (c *Client) GetProfileRelays() []string {
profileRelays := []string{}
// Skip the first relay (primary/theforest) and use fallback relays
if len(c.relays) > 1 {
profileRelays = append(profileRelays, c.relays[1:]...)
}
// If no fallback relays, return empty (shouldn't happen, but handle gracefully)
return profileRelays
return c.profileRelays
}
// GetContactRelays returns the relays for contact form submissions
func (c *Client) GetContactRelays() []string {
return c.contactRelays
}
// GetPool returns the underlying SimplePool (for services that need direct access)
@ -284,10 +282,10 @@ func (c *Client) FetchDeletionEvents(ctx context.Context, authors []string) (map @@ -284,10 +282,10 @@ func (c *Client) FetchDeletionEvents(ctx context.Context, authors []string) (map
return make(map[string]*nostr.Event), nil
}
// Fetch kind 5 deletion events from theforest only (primary relay)
primaryRelay := c.GetPrimaryRelay()
if primaryRelay == "" {
return nil, fmt.Errorf("primary relay not configured")
// Fetch kind 5 deletion events from profile relays
profileRelays := c.GetProfileRelays()
if len(profileRelays) == 0 {
return nil, fmt.Errorf("profile relays not configured")
}
filter := nostr.Filter{
@ -298,9 +296,10 @@ func (c *Client) FetchDeletionEvents(ctx context.Context, authors []string) (map @@ -298,9 +296,10 @@ func (c *Client) FetchDeletionEvents(ctx context.Context, authors []string) (map
logger.WithFields(map[string]interface{}{
"authors": len(uniqueAuthors),
}).Debug("Fetching deletion events")
"relays": len(profileRelays),
}).Debug("Fetching deletion events from profile relays")
deletionEvents, err := c.FetchEventsFromRelays(ctx, filter, []string{primaryRelay})
deletionEvents, err := c.FetchEventsFromRelays(ctx, filter, profileRelays)
if err != nil {
return nil, fmt.Errorf("failed to fetch deletion events: %w", err)
}

62
internal/server/handlers.go

@ -287,25 +287,73 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) { @@ -287,25 +287,73 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
return
}
// Validate event kind (will be validated again in PublishSignedIssue)
// Note: issueKind is stored in issueService, validation happens there
// Validate event kind (should be kind 1 for contact messages)
if req.Event.Kind != 1 {
http.Error(w, fmt.Sprintf("Invalid event kind: expected 1, got %d", req.Event.Kind), http.StatusBadRequest)
return
}
// Verify the event signature
valid, err := req.Event.CheckSignature()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to check signature: %v", err), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "Invalid event signature", http.StatusBadRequest)
return
}
// Publish the signed event
// Publish the signed event to contact relays
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
eventID, err := s.issueService.PublishSignedIssue(ctx, req.Event)
// Get contact relays
contactRelays := s.nostrClient.GetContactRelays()
// Publish to contact relays
var lastErr error
var published bool
for _, relayURL := range contactRelays {
relay, err := s.nostrClient.ConnectToRelay(ctx, relayURL)
if err != nil {
logger.Errorf("Failed to publish signed issue: %v", err)
logger.WithFields(map[string]interface{}{
"relay": relayURL,
"error": err,
}).Warn("Failed to connect to contact relay")
lastErr = err
continue
}
err = relay.Publish(ctx, *req.Event)
relay.Close()
if err != nil {
logger.WithFields(map[string]interface{}{
"relay": relayURL,
"error": err,
}).Warn("Failed to publish to contact relay")
lastErr = err
} else {
published = true
logger.WithFields(map[string]interface{}{
"relay": relayURL,
"event_id": req.Event.ID,
}).Info("Published contact event to relay")
}
}
if !published {
logger.Errorf("Failed to publish contact event to any relay: %v", lastErr)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"error": "Failed to publish issue: %s"}`, err.Error())
fmt.Fprintf(w, `{"error": "Failed to publish to any relay: %s"}`, lastErr.Error())
return
}
// Return success response
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"success": true, "event_id": "%s"}`, eventID)
fmt.Fprintf(w, `{"success": true, "event_id": "%s"}`, req.Event.ID)
}
// handleStatic serves static files

BIN
server

Binary file not shown.

79
static/css/main.css

@ -1039,6 +1039,85 @@ footer { @@ -1039,6 +1039,85 @@ footer {
display: block;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
}
.modal-content {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
}
.modal-close {
background: none;
border: none;
font-size: 2rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.modal-close:hover {
color: var(--text-primary);
}
.modal-body {
padding: 1.5rem;
}
.modal-body p {
margin-bottom: 1.5rem;
color: var(--text-secondary);
}
.modal-options {
display: flex;
flex-direction: column;
gap: 1rem;
}
.modal-options .btn {
width: 100%;
justify-content: center;
padding: 1rem;
}
.article-title {
font-size: 2.5rem;
margin: 0 0 0.5rem 0;

24
static/css/responsive.css

@ -764,6 +764,30 @@ @@ -764,6 +764,30 @@
.landing-page .feed-section {
margin: 1.5rem 0;
}
/* Modal */
.modal-content {
width: 95%;
max-width: 95%;
margin: 1rem;
}
.modal-header {
padding: 1rem;
}
.modal-header h2 {
font-size: 1.25rem;
}
.modal-body {
padding: 1rem;
}
.modal-options .btn {
padding: 0.875rem;
font-size: 0.95rem;
}
}
/* Tablet styles (768px - 1024px) */

217
templates/contact.html

@ -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">&times;</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();
function showModal() {
modal.style.display = 'flex';
}
// 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 hideModal() {
modal.style.display = 'none';
}
if (!repoAnnouncement) {
showStatus('Repository configuration not available. Please try again later.', true);
return;
// Close modal handlers
modalCloseBtn.addEventListener('click', hideModal);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
hideModal();
}
});
const subject = document.getElementById('subject').value.trim();
const content = document.getElementById('content').value.trim();
const labelsStr = document.getElementById('labels').value.trim();
// Generate key pair for anonymous submission
async function generateKeyPair() {
const keyPair = NostrTools.generatePrivateKey();
const pubkey = NostrTools.getPublicKey(keyPair);
return { privateKey: keyPair, pubkey: pubkey };
}
if (!subject || !content) {
showStatus('Subject and message are required.', true);
return;
// 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...';
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ event: signedEvent })
});
const result = await response.json();
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
// Add 'a' tag for repository announcement if available
if (repoAnnouncement) {
tags.push(['a', `30617:${repoAnnouncement.pubkey}:${repoAnnouncement.dTag}`]);
// Add 'p' tag for repository owner
tags.push(['p', repoAnnouncement.pubkey]);
// Add maintainers as 'p' tags
if (repoAnnouncement.maintainers && repoAnnouncement.maintainers.length > 0) {
repoAnnouncement.maintainers.forEach(maintainer => {
tags.push(['p', maintainer]);
});
}
}
// Add required 'p' tags for contact recipients
tags.push(['p', '846ebf79a0a8813274ec9727490621ad423f16a3e474d7fd66e6a98bfe4e39a4']);
tags.push(['p', 'fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1']);
// 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 })
});
const result = await response.json();
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);
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>

2
templates/feed.html

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
<h2>About TheForest Relay</h2>
<p>TheForest is a Nostr relay operated by GitCitadel. It provides a reliable, fast, and open relay service for the Nostr protocol.</p>
<ul>
<li><strong>Relay URL:</strong> <code>wss://theforest.nostr1.com</code></li>
<li><strong>Relay URL:</strong> <a href="https://theforest.nostr1.com" target="_blank" rel="noopener noreferrer"><code>wss://theforest.nostr1.com</code></a></li>
<li><strong>Status:</strong> Online and operational</li>
<li><strong>Features:</strong> Supports all standard Nostr event kinds</li>
</ul>

Loading…
Cancel
Save