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 @@ @@ -1,109 +1,290 @@
# 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
- Fetches wiki content from Nostr relays (kind 30818 events)
- Processes AsciiDoc content to HTML
- Caches all pages for fast serving
- Background cache rewarming to keep content fresh
- Kind 1 feed integration in sidebar
- SEO optimized with structured data
- Responsive design with medium-dark theme
- WCAG AA/AAA compliant accessibility
- YAML configuration for easy index management
- **Wiki System**: Fetches and displays wiki articles (kind 30818) from Nostr relays
- **Blog & Articles**: Supports blog posts and longform articles (kind 30023) with full markdown/AsciiDoc processing
- **E-Books Library**: Displays e-books and publications (kind 30040) from Nostr
- **Feed Integration**: Real-time kind 1 feed integration in sidebar
- **Contact Form**: Nostr-based contact form with browser extension support and anonymous submission
- **AsciiDoc Processing**: Full AsciiDoc to HTML conversion with table of contents support
- **Intelligent Caching**: Multi-layer caching system with background rewarming
- **Media Caching**: Automatic caching of external images and media
- **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
- Go 1.22+
- Node.js (for asciidoctor.js)
- @asciidoctor/core npm package
- Network access to Nostr relays
- **Go 1.22+** - For building and running the server
- **Node.js** - For AsciiDoc processing
- **@asciidoctor/core** - npm package for AsciiDoc conversion
- **Network access** - To connect to Nostr relays
## Installation
1. Clone the repository
2. Install Go dependencies:
1. **Clone the repository:**
```bash
git clone <repository-url>
cd gitcitadel-online
```
2. **Install Go dependencies:**
```bash
go mod tidy
```
3. Install Node.js dependencies:
3. **Install Node.js dependencies:**
```bash
npm install @asciidoctor/core
```
Or globally:
Or install globally:
```bash
npm install -g @asciidoctor/core
```
4. Download nostr-tools bundle (for contact form):
4. **Download nostr-tools bundle (for contact form):**
```bash
mkdir -p static/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.
5. Copy the example config:
5. **Copy and configure:**
```bash
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
Edit `config.yaml` to set:
Edit `config.yaml` to configure:
### Required Settings
- `wiki_index`: naddr for your wiki index (kind 30040)
- `blog_index`: naddr for your blog index (kind 30040)
- Relay URLs
- Cache refresh intervals
- Server port
- SEO settings
- `repo_announcement`: naddr for repository announcement (for contact form)
- `relays.feeds`: Primary relay URL for fetching content
- `relays.profiles`: Comma-separated relay URLs for profile data
- `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
### Development Mode
Run with verbose logging:
```bash
go run cmd/server/main.go
go run cmd/server/main.go --dev
```
Or build and run:
### Production Mode
Build and run:
```bash
go build -o gitcitadel-online cmd/server/main.go
./gitcitadel-online
```
Development mode with verbose logging:
```bash
go run cmd/server/main.go --dev
```
### Command Line Options
- `--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
```
gitcitadel-online/
├── cmd/server/ # Main server application
├── cmd/
│ └── server/ # Main server application entry point
├── 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
│ ├── asciidoc/ # AsciiDoc processing
│ ├── generator/ # HTML generation
│ ├── cache/ # Caching layer
│ ├── server/ # HTTP server
│ └── config/ # Configuration management
│ │ ├── client.go # Relay connection management
│ │ ├── wiki.go # Wiki event parsing
│ │ ├── profile.go # Profile metadata
│ │ ├── feed.go # Feed event parsing
│ │ ├── 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
├── static/ # Static assets (CSS, images)
└── config.yaml # Configuration file
│ ├── base.html # Base template
│ ├── 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:
- `/` - Landing page
- `/wiki/<d-tag>` - Wiki article pages
- `/blog` - Blog index page
- `/static/` - Static assets
- `/health` - Health check endpoint
- `/metrics` - Metrics endpoint
- `/sitemap.xml` - Sitemap
- `/robots.txt` - Robots.txt
### Building
```bash
go build -o gitcitadel-online cmd/server/main.go
```
### Testing
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
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 ( @@ -20,12 +20,13 @@ import (
func getTemplateFuncs() template.FuncMap {
return template.FuncMap{
"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)
if err != nil {
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 {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
@ -814,7 +815,8 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event @@ -814,7 +815,8 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
}
// 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{}{
"Pubkey": repoAnnouncement.Pubkey,
"DTag": repoAnnouncement.DTag,

10
internal/nostr/issues.go

@ -40,6 +40,11 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem @@ -40,6 +40,11 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem
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{
Event: event,
Pubkey: event.PubKey,
@ -53,6 +58,11 @@ func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncem @@ -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
for _, tag := range event.Tags {
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) { @@ -275,6 +275,7 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
// Parse JSON request
var req struct {
Event *gonostr.Event `json:"event"`
AdditionalRelays []string `json:"additionalRelays,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -287,9 +288,9 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) { @@ -287,9 +288,9 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
return
}
// 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)
// Validate event kind (should be kind 1621 for issues per NIP-34)
if req.Event.Kind != 1621 {
http.Error(w, fmt.Sprintf("Invalid event kind: expected 1621, got %d", req.Event.Kind), http.StatusBadRequest)
return
}
@ -311,10 +312,28 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) { @@ -311,10 +312,28 @@ func (s *Server) handleContactAPI(w http.ResponseWriter, r *http.Request) {
// Get contact relays
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 published bool
for _, relayURL := range contactRelays {
for _, relayURL := range relaysToPublish {
relay, err := s.nostrClient.ConnectToRelay(ctx, relayURL)
if err != nil {
logger.WithFields(map[string]interface{}{

169
templates/contact.html

@ -207,10 +207,46 @@ @@ -207,10 +207,46 @@
const repoDataEl = document.getElementById('repo-announcement-data');
if (repoDataEl) {
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) {
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
@ -285,9 +321,9 @@ @@ -285,9 +321,9 @@
// Generate key pair for anonymous submission
async function generateKeyPair() {
const keyPair = NostrTools.generatePrivateKey();
const pubkey = NostrTools.getPublicKey(keyPair);
return { privateKey: keyPair, pubkey: pubkey };
const secretKey = NostrTools.generateSecretKey();
const pubkey = NostrTools.getPublicKey(secretKey);
return { privateKey: secretKey, pubkey: pubkey };
}
// Sign event with private key
@ -295,16 +331,39 @@ @@ -295,16 +331,39 @@
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
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...';
// Include additional relays (outbox relays from user's relay list)
const eventWithRelays = {
...signedEvent,
additionalRelays: additionalRelays
};
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ event: signedEvent })
body: JSON.stringify({ event: eventWithRelays })
});
const result = await response.json();
@ -332,6 +391,8 @@ @@ -332,6 +391,8 @@
let pubkey;
let signFunction;
let userOutboxRelays = [];
if (useExtension) {
// Login with browser extension
if (!window.nostr) {
@ -344,6 +405,19 @@ @@ -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...';
pubkey = await window.nostr.getPublicKey();
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 {
// 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...';
@ -360,60 +434,72 @@ @@ -360,60 +434,72 @@
// Build event tags
const tags = [];
// Add 'a' tag for repository announcement if available
if (repoAnnouncement && repoAnnouncement.pubkey && repoAnnouncement.dTag) {
tags.push(['a', `30617:${repoAnnouncement.pubkey}:${repoAnnouncement.dTag}`]);
tags.push(['p', repoAnnouncement.pubkey]);
// Add 'a' tag for repository announcement if available (required for NIP-34 issues)
// Format: ["a", "30617:<pubkey>:<d-tag>"]
// Note: Field names are capitalized (Pubkey, DTag) as they come from Go
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) {
repoAnnouncement.maintainers.forEach(maintainer => {
if (maintainer) {
tags.push(['p', maintainer]);
// Add maintainers (deduplicated - owner won't be added twice)
if (repoAnnouncement.Maintainers && Array.isArray(repoAnnouncement.Maintainers)) {
repoAnnouncement.Maintainers.forEach(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
tags.push(['client', 'gitcitadel.com']);
// Add all unique pubkeys as 'p' tags
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) {
tags.push(['subject', subject]);
}
// Add label tags
// Add label tags (t tags for issue labels per NIP-34)
labels.forEach(label => {
if (label) {
tags.push(['t', label]);
}
});
// Add contact relays tag
// Store relays as a JSON string in the tag value
if (contactRelays && contactRelays.length > 0) {
tags.push(['relays', JSON.stringify(contactRelays)]);
}
// Add client tag
tags.push(['client', 'GitCitadel.com']);
// Create unsigned event (kind 1 for contact messages)
// Create unsigned event (kind 1621 for issues per NIP-34)
const unsignedEvent = {
kind: 1,
kind: 1621,
pubkey: pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: tags,
content: `Subject: ${subject}\n\n${content}`
content: content // Just the content, subject is in tags
};
// 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...';
const signedEvent = await signFunction(unsignedEvent);
// Submit event
await submitEvent(signedEvent);
// Submit event with user's outbox relays
await submitEvent(signedEvent, userOutboxRelays);
} catch (error) {
console.error('Error:', error);
showFailureModal('Error: ' + error.message);
@ -430,8 +516,23 @@ @@ -430,8 +516,23 @@
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Validate repo announcement data
if (!repoAnnouncement) {
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;
}

Loading…
Cancel
Save