Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
c6b4ed44e4
  1. 283
      README.md
  2. 8
      internal/generator/html.go
  3. 10
      internal/nostr/issues.go
  4. 29
      internal/server/handlers.go
  5. 169
      templates/contact.html

283
README.md

@ -1,109 +1,290 @@
# GitCitadel Online # GitCitadel Online
A server-generated website that fetches kind 30818 wiki events from Nostr relays, processes AsciiDoc content, and serves professional HTML pages with caching. A server-generated static website that fetches content from Nostr relays, processes AsciiDoc articles, and serves professional HTML pages with intelligent caching. Built with Go and designed for decentralized content publishing.
## Features ## Features
- Fetches wiki content from Nostr relays (kind 30818 events) - **Wiki System**: Fetches and displays wiki articles (kind 30818) from Nostr relays
- Processes AsciiDoc content to HTML - **Blog & Articles**: Supports blog posts and longform articles (kind 30023) with full markdown/AsciiDoc processing
- Caches all pages for fast serving - **E-Books Library**: Displays e-books and publications (kind 30040) from Nostr
- Background cache rewarming to keep content fresh - **Feed Integration**: Real-time kind 1 feed integration in sidebar
- Kind 1 feed integration in sidebar - **Contact Form**: Nostr-based contact form with browser extension support and anonymous submission
- SEO optimized with structured data - **AsciiDoc Processing**: Full AsciiDoc to HTML conversion with table of contents support
- Responsive design with medium-dark theme - **Intelligent Caching**: Multi-layer caching system with background rewarming
- WCAG AA/AAA compliant accessibility - **Media Caching**: Automatic caching of external images and media
- YAML configuration for easy index management - **SEO Optimized**: Structured data, sitemaps, and meta tags
- **Responsive Design**: Mobile-first responsive design with medium-dark theme
- **Accessibility**: WCAG AA/AAA compliant with proper ARIA labels and keyboard navigation
- **Content Security Policy**: Secure CSP headers for XSS protection
## Requirements ## Requirements
- Go 1.22+ - **Go 1.22+** - For building and running the server
- Node.js (for asciidoctor.js) - **Node.js** - For AsciiDoc processing
- @asciidoctor/core npm package - **@asciidoctor/core** - npm package for AsciiDoc conversion
- Network access to Nostr relays - **Network access** - To connect to Nostr relays
## Installation ## Installation
1. Clone the repository 1. **Clone the repository:**
2. Install Go dependencies: ```bash
git clone <repository-url>
cd gitcitadel-online
```
2. **Install Go dependencies:**
```bash ```bash
go mod tidy go mod tidy
``` ```
3. Install Node.js dependencies:
3. **Install Node.js dependencies:**
```bash ```bash
npm install @asciidoctor/core npm install @asciidoctor/core
``` ```
Or globally: Or install globally:
```bash ```bash
npm install -g @asciidoctor/core npm install -g @asciidoctor/core
``` ```
4. Download nostr-tools bundle (for contact form):
4. **Download nostr-tools bundle (for contact form):**
```bash ```bash
mkdir -p static/js mkdir -p static/js
curl -L -o static/js/nostr.bundle.js https://unpkg.com/nostr-tools@latest/lib/nostr.bundle.js curl -L -o static/js/nostr.bundle.js https://unpkg.com/nostr-tools@latest/lib/nostr.bundle.js
``` ```
Note: The nostr-tools library is hosted locally to avoid dependency on external CDNs. Note: The nostr-tools library is hosted locally to avoid dependency on external CDNs.
5. Copy the example config:
5. **Copy and configure:**
```bash ```bash
cp config.yaml.example config.yaml cp config.yaml.example config.yaml
``` ```
6. Edit `config.yaml` with your indices and settings Edit `config.yaml` with your Nostr indices, relay URLs, and settings.
## Configuration ## Configuration
Edit `config.yaml` to set: Edit `config.yaml` to configure:
### Required Settings
- `wiki_index`: naddr for your wiki index (kind 30040) - `wiki_index`: naddr for your wiki index (kind 30040)
- `blog_index`: naddr for your blog index (kind 30040) - `blog_index`: naddr for your blog index (kind 30040)
- Relay URLs - `repo_announcement`: naddr for repository announcement (for contact form)
- Cache refresh intervals - `relays.feeds`: Primary relay URL for fetching content
- Server port - `relays.profiles`: Comma-separated relay URLs for profile data
- SEO settings - `relays.contactform`: Comma-separated relay URLs for contact form submissions
### Optional Settings
- `link_base_url`: Base URL for external links (default: Alexandria)
- `cache.refresh_interval_minutes`: How often to refresh cached pages (default: 30)
- `feed.poll_interval_minutes`: How often to poll for new feed items (default: 5)
- `feed.max_events`: Maximum number of feed items to display (default: 30)
- `server.port`: HTTP server port (default: 8080)
- `server.enable_compression`: Enable gzip compression (default: true)
- `seo.site_name`: Site name for SEO
- `seo.site_url`: Canonical site URL
- `seo.default_image`: Default OpenGraph image path
### Example Configuration
```yaml
wiki_index: "naddr1qvzqqqr4tqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqyd8wumn8ghj7..."
blog_index: "naddr1qvzqqqr4tqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqyvhwumn8ghj7..."
repo_announcement: "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqq9xw6t5vd5hgctyv4kqde47kt"
relays:
feeds: "wss://theforest.nostr1.com"
profiles: "wss://theforest.nostr1.com,wss://nostr.land"
contactform: "wss://thecitadel.nostr1.com,wss://relay.damus.io"
server:
port: 8080
enable_compression: true
seo:
site_name: "GitCitadel"
site_url: "https://gitcitadel.com"
```
## Running ## Running
### Development Mode
Run with verbose logging:
```bash ```bash
go run cmd/server/main.go go run cmd/server/main.go --dev
``` ```
Or build and run: ### Production Mode
Build and run:
```bash ```bash
go build -o gitcitadel-online cmd/server/main.go go build -o gitcitadel-online cmd/server/main.go
./gitcitadel-online ./gitcitadel-online
``` ```
Development mode with verbose logging: ### Command Line Options
```bash
go run cmd/server/main.go --dev - `--config <path>`: Path to configuration file (default: `config.yaml`)
``` - `--dev`: Enable development mode with verbose logging
- `--log-level <level>`: Set log level (debug, info, warn, error) (default: info)
## Routes & Endpoints
### Public Pages
- `/` - Landing page with feed sidebar
- `/wiki` - Wiki index page
- `/wiki/<d-tag>` - Individual wiki article pages
- `/blog` - Blog index page with article navigation
- `/articles` - Longform articles index page
- `/ebooks` - E-books library with sortable table
- `/feed` - Feed page with relay information
- `/contact` - Contact form with Nostr integration
### Static Assets
- `/static/` - Static files (CSS, JavaScript, images, icons)
- `/cache/media/` - Cached external media files
- `/favicon.ico` - Site favicon
### API Endpoints
- `/api/contact` - POST endpoint for submitting contact form events (JSON)
### System Endpoints
- `/health` - Health check endpoint
- `/metrics` - Metrics endpoint (Prometheus format)
- `/sitemap.xml` - XML sitemap for search engines
- `/robots.txt` - Robots.txt file
## Project Structure ## Project Structure
``` ```
gitcitadel-online/ gitcitadel-online/
├── cmd/server/ # Main server application ├── cmd/
│ └── server/ # Main server application entry point
├── internal/ ├── internal/
│ ├── asciidoc/ # AsciiDoc processing with Node.js
│ ├── cache/ # Multi-layer caching system
│ │ ├── cache.go # Page cache
│ │ ├── feed_cache.go # Feed item cache
│ │ └── media_cache.go # Media file cache
│ ├── config/ # Configuration management
│ ├── generator/ # HTML generation and SEO
│ ├── logger/ # Structured logging
│ ├── nostr/ # Nostr client and event parsing │ ├── nostr/ # Nostr client and event parsing
│ ├── asciidoc/ # AsciiDoc processing │ │ ├── client.go # Relay connection management
│ ├── generator/ # HTML generation │ │ ├── wiki.go # Wiki event parsing
│ ├── cache/ # Caching layer │ │ ├── profile.go # Profile metadata
│ ├── server/ # HTTP server │ │ ├── feed.go # Feed event parsing
│ └── config/ # Configuration management │ │ ├── ebooks.go # E-book parsing
│ │ └── issues.go # Issue/contact form handling
│ └── server/ # HTTP server and handlers
├── static/ # Static assets
│ ├── css/ # Stylesheets
│ ├── icons/ # SVG icons
│ └── js/ # JavaScript libraries
├── templates/ # HTML templates ├── templates/ # HTML templates
├── static/ # Static assets (CSS, images) │ ├── base.html # Base template
└── config.yaml # Configuration file │ ├── landing.html # Landing page
│ ├── wiki.html # Wiki pages
│ ├── blog.html # Blog pages
│ ├── articles.html # Article pages
│ ├── ebooks.html # E-books page
│ ├── feed.html # Feed page
│ ├── contact.html # Contact form
│ └── components.html # Reusable components
├── cache/ # Runtime cache directory
│ └── media/ # Cached media files
├── config.yaml # Configuration file (not in repo)
├── config.yaml.example # Example configuration
├── go.mod # Go module dependencies
├── package.json # Node.js dependencies
└── README.md # This file
``` ```
## API ## Development
The server provides: ### Building
- `/` - Landing page
- `/wiki/<d-tag>` - Wiki article pages ```bash
- `/blog` - Blog index page go build -o gitcitadel-online cmd/server/main.go
- `/static/` - Static assets ```
- `/health` - Health check endpoint
- `/metrics` - Metrics endpoint ### Testing
- `/sitemap.xml` - Sitemap
- `/robots.txt` - Robots.txt The server uses a caching system that pre-generates all pages. On first run, pages will be generated and cached. Subsequent requests serve from cache until the refresh interval.
### Cache Management
- Pages are cached in memory for fast serving
- Cache rewarming runs in the background at configured intervals
- Media files are cached to disk in `cache/media/`
- Cache can be cleared by restarting the server
### Logging
Logs are structured and can be configured via:
- `--log-level` flag (debug, info, warn, error)
- `--dev` flag enables debug logging and verbose output
## Content Types Supported
### Wiki Articles (Kind 30818)
- AsciiDoc content processing
- Table of contents generation
- Cross-referencing support
- Syntax highlighting
### Blog Posts (Kind 30023)
- Markdown/AsciiDoc content
- Image support with caching
- Author profiles
- Timestamps and metadata
### E-Books (Kind 30040)
- Publication listings
- Author information
- Sortable table interface
- Links to Alexandria library
### Feed Items (Kind 1)
- Real-time note display
- Author badges with profiles
- Timestamp formatting
- Content rendering
## Contact Form
The contact form supports two submission methods:
1. **Browser Extension**: Users can sign with their Nostr browser extension (nos2x, Alby, etc.)
2. **Anonymous**: Server generates a temporary key pair for anonymous submissions
Both methods publish kind 1 events to configured relays with proper tags for issue tracking.
## Security
- Content Security Policy (CSP) headers prevent XSS attacks
- All external scripts are hosted locally
- Input validation on contact form
- Event signature verification for API submissions
- Secure relay connections (WSS)
## Performance
- Multi-layer caching (memory + disk)
- Background cache rewarming
- Gzip compression support
- Optimized static asset serving
- Efficient Nostr event parsing
## License ## License
MIT License - see LICENSE.md MIT License - see LICENSE.md for details
## Contributing
Contributions are welcome! Please ensure:
- Code follows Go conventions
- Templates are accessible (WCAG AA/AAA)
- All routes are documented
- Configuration changes are backward compatible

8
internal/generator/html.go

@ -20,12 +20,13 @@ import (
func getTemplateFuncs() template.FuncMap { func getTemplateFuncs() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"year": func() int { return time.Now().Year() }, "year": func() int { return time.Now().Year() },
"json": func(v interface{}) (string, error) { "json": func(v interface{}) (template.HTML, error) {
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil { if err != nil {
return "", err return "", err
} }
return string(b), nil // Return as template.HTML to prevent HTML escaping
return template.HTML(b), nil
}, },
"hasPrefix": func(s, prefix string) bool { "hasPrefix": func(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix return len(s) >= len(prefix) && s[:len(prefix)] == prefix
@ -814,7 +815,8 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
} }
// Add repo announcement data for JavaScript // Add repo announcement data for JavaScript
if repoAnnouncement != nil { // Only include if Pubkey and DTag are both set (required fields)
if repoAnnouncement != nil && repoAnnouncement.Pubkey != "" && repoAnnouncement.DTag != "" {
templateData["RepoAnnouncement"] = map[string]interface{}{ templateData["RepoAnnouncement"] = map[string]interface{}{
"Pubkey": repoAnnouncement.Pubkey, "Pubkey": repoAnnouncement.Pubkey,
"DTag": repoAnnouncement.DTag, "DTag": repoAnnouncement.DTag,

10
internal/nostr/issues.go

@ -40,6 +40,11 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem
return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind) return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind)
} }
// Validate that PubKey is set
if event.PubKey == "" {
return nil, fmt.Errorf("repository announcement event missing pubkey")
}
repo := &RepoAnnouncement{ repo := &RepoAnnouncement{
Event: event, Event: event,
Pubkey: event.PubKey, Pubkey: event.PubKey,
@ -53,6 +58,11 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem
} }
} }
// Validate that DTag is set
if repo.DTag == "" {
return nil, fmt.Errorf("repository announcement event missing d tag")
}
// Extract relays tag // Extract relays tag
for _, tag := range event.Tags { for _, tag := range event.Tags {
if len(tag) > 0 && tag[0] == "relays" && len(tag) > 1 { if len(tag) > 0 && tag[0] == "relays" && len(tag) > 1 {

29
internal/server/handlers.go

@ -275,6 +275,7 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
// Parse JSON request // Parse JSON request
var req struct { var req struct {
Event *gonostr.Event `json:"event"` Event *gonostr.Event `json:"event"`
AdditionalRelays []string `json:"additionalRelays,omitempty"`
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -287,9 +288,9 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
return return
} }
// Validate event kind (should be kind 1 for contact messages) // Validate event kind (should be kind 1621 for issues per NIP-34)
if req.Event.Kind != 1 { if req.Event.Kind != 1621 {
http.Error(w, fmt.Sprintf("Invalid event kind: expected 1, got %d", req.Event.Kind), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Invalid event kind: expected 1621, got %d", req.Event.Kind), http.StatusBadRequest)
return return
} }
@ -311,10 +312,28 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
// Get contact relays // Get contact relays
contactRelays := s.nostrClient.GetContactRelays() contactRelays := s.nostrClient.GetContactRelays()
// Publish to contact relays // Combine contact relays with user's outbox relays (if provided)
allRelays := make(map[string]bool)
for _, relay := range contactRelays {
allRelays[relay] = true
}
// Add user's outbox relays (from their kind 10002 relay list)
for _, relay := range req.AdditionalRelays {
if relay != "" {
allRelays[relay] = true
}
}
// Convert map to slice
relaysToPublish := make([]string, 0, len(allRelays))
for relay := range allRelays {
relaysToPublish = append(relaysToPublish, relay)
}
// Publish to all relays (contact relays + user outbox relays)
var lastErr error var lastErr error
var published bool var published bool
for _, relayURL := range contactRelays { for _, relayURL := range relaysToPublish {
relay, err := s.nostrClient.ConnectToRelay(ctx, relayURL) relay, err := s.nostrClient.ConnectToRelay(ctx, relayURL)
if err != nil { if err != nil {
logger.WithFields(map[string]interface{}{ logger.WithFields(map[string]interface{}{

169
templates/contact.html

@ -207,10 +207,46 @@
const repoDataEl = document.getElementById('repo-announcement-data'); const repoDataEl = document.getElementById('repo-announcement-data');
if (repoDataEl) { if (repoDataEl) {
try { try {
repoAnnouncement = JSON.parse(repoDataEl.textContent); // Trim whitespace before parsing
let jsonText = repoDataEl.textContent.trim();
let parsed = JSON.parse(jsonText);
// Handle double-encoded JSON (if the result is still a string, parse again)
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
// Ensure all required fields are present and valid
const pubkey = parsed?.Pubkey;
const dTag = parsed?.DTag;
const hasPubkey = pubkey && typeof pubkey === 'string' && pubkey.trim() !== '';
const hasDTag = dTag && typeof dTag === 'string' && dTag.trim() !== '';
if (hasPubkey && hasDTag) {
repoAnnouncement = parsed;
console.log('Repo announcement loaded successfully:', {
dTag: dTag,
pubkey: pubkey.substring(0, 16) + '...',
maintainersCount: parsed.Maintainers ? parsed.Maintainers.length : 0,
relaysCount: parsed.Relays ? parsed.Relays.length : 0
});
} else {
console.error('Repo announcement data incomplete:', {
hasPubkey,
hasDTag,
pubkeyType: typeof pubkey,
pubkeyValue: pubkey,
dTagType: typeof dTag,
dTagValue: dTag,
fullObject: parsed
});
}
} catch (e) { } catch (e) {
console.error('Failed to parse repo announcement data:', e); console.error('Failed to parse repo announcement data:', e, 'Raw content:', repoDataEl.textContent);
} }
} else {
console.warn('Repo announcement data element not found');
} }
// Store form data for submission // Store form data for submission
@ -285,9 +321,9 @@
// Generate key pair for anonymous submission // Generate key pair for anonymous submission
async function generateKeyPair() { async function generateKeyPair() {
const keyPair = NostrTools.generatePrivateKey(); const secretKey = NostrTools.generateSecretKey();
const pubkey = NostrTools.getPublicKey(keyPair); const pubkey = NostrTools.getPublicKey(secretKey);
return { privateKey: keyPair, pubkey: pubkey }; return { privateKey: secretKey, pubkey: pubkey };
} }
// Sign event with private key // Sign event with private key
@ -295,16 +331,39 @@
return NostrTools.finalizeEvent(event, privateKey); return NostrTools.finalizeEvent(event, privateKey);
} }
// Extract outbox (write) relays from user's relay list (kind 10002)
function extractOutboxRelays(relayListEvent) {
const outboxRelays = [];
if (relayListEvent && relayListEvent.tags) {
for (const tag of relayListEvent.tags) {
// Format: ["r", "<relay-url>", "write"] for outbox relays
if (tag[0] === 'r' && tag.length >= 3 && tag[2] === 'write') {
const relayUrl = tag[1];
if (relayUrl && !outboxRelays.includes(relayUrl)) {
outboxRelays.push(relayUrl);
}
}
}
}
return outboxRelays;
}
// Submit event // Submit event
async function submitEvent(signedEvent) { async function submitEvent(signedEvent, additionalRelays = []) {
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...'; 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...';
// Include additional relays (outbox relays from user's relay list)
const eventWithRelays = {
...signedEvent,
additionalRelays: additionalRelays
};
const response = await fetch('/api/contact', { const response = await fetch('/api/contact', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ event: signedEvent }) body: JSON.stringify({ event: eventWithRelays })
}); });
const result = await response.json(); const result = await response.json();
@ -332,6 +391,8 @@
let pubkey; let pubkey;
let signFunction; let signFunction;
let userOutboxRelays = [];
if (useExtension) { if (useExtension) {
// Login with browser extension // Login with browser extension
if (!window.nostr) { if (!window.nostr) {
@ -344,6 +405,19 @@
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...'; 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(); pubkey = await window.nostr.getPublicKey();
signFunction = (event) => window.nostr.signEvent(event); signFunction = (event) => window.nostr.signEvent(event);
// Fetch user's relay list (kind 10002) to get outbox relays
if (window.nostr.getRelays) {
try {
const relayListEvent = await window.nostr.getRelays();
if (relayListEvent) {
userOutboxRelays = extractOutboxRelays(relayListEvent);
}
} catch (e) {
console.warn('Failed to fetch user relay list:', e);
// Continue without user relay list - not critical
}
}
} else { } else {
// Anonymous submission - generate key pair // 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...'; 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...';
@ -360,60 +434,72 @@
// Build event tags // Build event tags
const tags = []; const tags = [];
// Add 'a' tag for repository announcement if available // Add 'a' tag for repository announcement if available (required for NIP-34 issues)
if (repoAnnouncement && repoAnnouncement.pubkey && repoAnnouncement.dTag) { // Format: ["a", "30617:<pubkey>:<d-tag>"]
tags.push(['a', `30617:${repoAnnouncement.pubkey}:${repoAnnouncement.dTag}`]); // Note: Field names are capitalized (Pubkey, DTag) as they come from Go
tags.push(['p', repoAnnouncement.pubkey]); const repoPubkey = repoAnnouncement?.Pubkey;
const repoDTag = repoAnnouncement?.DTag;
if (repoAnnouncement && repoPubkey && typeof repoPubkey === 'string' && repoPubkey.trim() !== '' &&
repoDTag && typeof repoDTag === 'string' && repoDTag.trim() !== '') {
tags.push(['a', `30617:${repoPubkey.trim()}:${repoDTag.trim()}`]);
// Collect unique pubkeys for 'p' tags (owner + maintainers, deduplicated)
const uniquePubkeys = new Set();
// Add repository owner (required for NIP-34 issues)
uniquePubkeys.add(repoPubkey.trim());
if (repoAnnouncement.maintainers && repoAnnouncement.maintainers.length > 0) { // Add maintainers (deduplicated - owner won't be added twice)
repoAnnouncement.maintainers.forEach(maintainer => { if (repoAnnouncement.Maintainers && Array.isArray(repoAnnouncement.Maintainers)) {
if (maintainer) { repoAnnouncement.Maintainers.forEach(maintainer => {
tags.push(['p', maintainer]); if (maintainer && typeof maintainer === 'string' && maintainer.trim() !== '') {
uniquePubkeys.add(maintainer.trim());
} }
}); });
} }
}
// Add required 'p' tags for contact recipients
tags.push(['p', '846ebf79a0a8813274ec9727490621ad423f16a3e474d7fd66e6a98bfe4e39a4']);
tags.push(['p', 'fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1']);
// Add client tag // Add all unique pubkeys as 'p' tags
tags.push(['client', 'gitcitadel.com']); uniquePubkeys.forEach(pk => {
tags.push(['p', pk]);
});
} else {
// This should not happen if validation passed, but log for debugging
console.error('Repo announcement data missing or incomplete in tag building:', JSON.stringify(repoAnnouncement));
showFailureModal('Repository configuration is invalid. Please refresh the page and try again.');
return;
}
// Add subject tag // Add subject tag (required for NIP-34 issues)
if (subject) { if (subject) {
tags.push(['subject', subject]); tags.push(['subject', subject]);
} }
// Add label tags // Add label tags (t tags for issue labels per NIP-34)
labels.forEach(label => { labels.forEach(label => {
if (label) { if (label) {
tags.push(['t', label]); tags.push(['t', label]);
} }
}); });
// Add contact relays tag // Add client tag
// Store relays as a JSON string in the tag value tags.push(['client', 'GitCitadel.com']);
if (contactRelays && contactRelays.length > 0) {
tags.push(['relays', JSON.stringify(contactRelays)]);
}
// Create unsigned event (kind 1 for contact messages) // Create unsigned event (kind 1621 for issues per NIP-34)
const unsignedEvent = { const unsignedEvent = {
kind: 1, kind: 1621,
pubkey: pubkey, pubkey: pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: tags, tags: tags,
content: `Subject: ${subject}\n\n${content}` content: content // Just the content, subject is in tags
}; };
// Sign the event // 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> Signing...'; 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 signedEvent = await signFunction(unsignedEvent);
// Submit event // Submit event with user's outbox relays
await submitEvent(signedEvent); await submitEvent(signedEvent, userOutboxRelays);
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
showFailureModal('Error: ' + error.message); showFailureModal('Error: ' + error.message);
@ -430,8 +516,23 @@
form.addEventListener('submit', async function(e) { form.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
// Validate repo announcement data
if (!repoAnnouncement) { if (!repoAnnouncement) {
showStatus('Repository configuration not available. Please try again later.', true); showStatus('Repository configuration not available. Please try again later.', true);
console.error('Repo announcement is null or undefined');
return;
}
// Additional validation (should already be validated during parsing, but double-check)
if (!repoAnnouncement.Pubkey || typeof repoAnnouncement.Pubkey !== 'string' || repoAnnouncement.Pubkey.trim() === '') {
showStatus('Repository configuration incomplete: missing pubkey. Please try again later.', true);
console.error('Repo announcement missing Pubkey:', JSON.stringify(repoAnnouncement));
return;
}
if (!repoAnnouncement.DTag || typeof repoAnnouncement.DTag !== 'string' || repoAnnouncement.DTag.trim() === '') {
showStatus('Repository configuration incomplete: missing d-tag. Please try again later.', true);
console.error('Repo announcement missing DTag:', JSON.stringify(repoAnnouncement));
return; return;
} }

Loading…
Cancel
Save