Compare commits

...

3 Commits

Author SHA1 Message Date
96bdf5cba2 Implement Tag-based e/p model for Neo4j backend (v0.36.0)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add unified Tag-based model where e/p tags create intermediate Tag nodes
  with REFERENCES relationships to Event/NostrUser nodes
- Update save-event.go: addPTagsInBatches and addETagsInBatches now create
  Tag nodes with TAGGED_WITH and REFERENCES relationships
- Update delete.go: CheckForDeleted uses Tag traversal for kind 5 detection
- Add v3 migration in migrations.go to convert existing direct REFERENCES
  and MENTIONS relationships to the new Tag-based model
- Create comprehensive test file tag_model_test.go with 15+ test functions
  covering Tag model, filter queries, migrations, and deletion detection
- Update save-event_test.go to verify new Tag-based relationship patterns
- Update WOT_SPEC.md with Tag-Based References documentation section
- Update CLAUDE.md and README.md with Neo4j Tag-based model documentation
- Bump version to v0.36.0

This change enables #e and #p filter queries to work correctly by storing
all tags (including e/p) through intermediate Tag nodes.

Files modified:
- pkg/neo4j/save-event.go: Tag-based e/p relationship creation
- pkg/neo4j/delete.go: Tag traversal for deletion detection
- pkg/neo4j/migrations.go: v3 migration for existing data
- pkg/neo4j/tag_model_test.go: New comprehensive test file
- pkg/neo4j/save-event_test.go: Updated for new model
- pkg/neo4j/WOT_SPEC.md: Tag-Based References documentation
- pkg/neo4j/README.md: Architecture and example queries
- CLAUDE.md: Repository documentation update
- pkg/version/version: Bump to v0.36.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 09:22:05 +01:00
516ce9c42c Add issue templates, CI workflows, and decentralization plan
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add Gitea issue templates for bug reports and feature requests with
  structured YAML forms for version, database backend, and log level
- Add GitHub Actions CI workflow for automated testing on push/PR
- Add GitHub Actions release workflow for building multi-platform
  binaries on tag push with SHA256 checksums
- Add CONTRIBUTING.md with development setup, PR guidelines, and
  commit message format documentation
- Add DECENTRALIZE_NOSTR.md expansion plan outlining WireGuard tunnel,
  GUI installer, system tray, and proxy server architecture
- Update allowed commands in Claude settings
- Bump version to v0.35.5

Files modified:
- .gitea/issue_template/: Bug report, feature request, and config YAML
- .github/workflows/: CI and release automation workflows
- CONTRIBUTING.md: New contributor guide
- docs/plans/DECENTRALIZE_NOSTR.md: Expansion architecture plan
- .claude/settings.local.json: Updated allowed commands
- pkg/version/version: Version bump to v0.35.5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 20:50:49 +01:00
ed95947971 Add release command and bump version to v0.35.4
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add .claude/commands/release.md slash command for automated release
  workflow with version bumping, commit creation, tagging, and push
- Supports patch and minor version increments with proper validation
- Includes build verification step before committing
- Update settings.local.json with allowed commands from previous session
- Bump version from v0.35.3 to v0.35.4

Files modified:
- .claude/commands/release.md: New release automation command
- .claude/settings.local.json: Updated allowed commands
- pkg/version/version: Version bump to v0.35.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 19:50:13 +01:00
18 changed files with 2388 additions and 59 deletions

View File

@@ -0,0 +1,50 @@
# Release Command
Review all changes in the repository and create a release with proper commit message, version tag, and push to remotes.
## Argument: $ARGUMENTS
The argument should be one of:
- `patch` - Bump the patch version (e.g., v0.35.3 -> v0.35.4)
- `minor` - Bump the minor version and reset patch to 0 (e.g., v0.35.3 -> v0.36.0)
If no argument provided, default to `patch`.
## Steps to perform:
1. **Read the current version** from `pkg/version/version`
2. **Calculate the new version** based on the argument:
- Parse the current version (format: vMAJOR.MINOR.PATCH)
- If `patch`: increment PATCH by 1
- If `minor`: increment MINOR by 1, set PATCH to 0
3. **Update the version file** (`pkg/version/version`) with the new version
4. **Review changes** using `git status` and `git diff --stat HEAD`
5. **Compose a commit message** following this format:
- First line: 72 chars max, imperative mood summary
- Blank line
- Bullet points describing each significant change
- "Files modified:" section listing affected files
- Footer with Claude Code attribution
6. **Stage all changes** with `git add -A`
7. **Create the commit** with the composed message
8. **Create a git tag** with the new version (e.g., `v0.36.0`)
9. **Push to remotes** (origin and gitea) with tags:
```
git push origin main --tags
git push gitea main --tags
```
10. **Report completion** with the new version and commit hash
## Important:
- Do NOT push to github remote (only origin and gitea)
- Always verify the build compiles before committing: `CGO_ENABLED=0 go build -o /dev/null ./...`
- If build fails, fix issues before proceeding

View File

@@ -117,7 +117,10 @@
"Bash(go run -exec '' -c 'package main; import \"\"git.mleku.dev/mleku/nostr/utils/normalize\"\"; import \"\"fmt\"\"; func main() { fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:443\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"ws://relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"wss://relay.example.com:3334\"\")))) }')",
"Bash(go run:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nFix NIP-11 fetch URL scheme conversion for non-proxied relays\n\n- Convert wss:// to https:// and ws:// to http:// before fetching NIP-11\n documents, fixing failures for users not using HTTPS upgrade proxies\n- The fetchNIP11 function was using WebSocket URLs directly for HTTP\n requests, causing scheme mismatch errors\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(/tmp/orly help:*)"
"Bash(/tmp/orly help:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nAdd ORLY_POLICY_PATH for custom policy file location\n\n- Add ORLY_POLICY_PATH environment variable to configure custom policy\n file path, overriding the default ~/.config/ORLY/policy.json location\n- Enforce ABSOLUTE paths only - relay panics on startup if relative path\n is provided, preventing common misconfiguration errors\n- Update PolicyManager to store and expose configPath for hot-reload saves\n- Add ConfigPath() method to P struct delegating to internal PolicyManager\n- Update NewWithManager() signature to accept optional custom path parameter\n- Add BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md with issue submission\n guidelines requiring environment details, reproduction steps, and logs\n- Update README.md with system requirements (500MB minimum memory) and\n link to bug report protocol\n- Update CLAUDE.md and README.md documentation for new ORLY_POLICY_PATH\n\nFiles modified:\n- app/config/config.go: Add PolicyPath config field\n- pkg/policy/policy.go: Add configPath storage and validation\n- app/handle-policy-config.go: Use policyManager.ConfigPath()\n- app/main.go: Pass cfg.PolicyPath to NewWithManager\n- pkg/policy/*_test.go: Update test calls with new parameter\n- BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md: New file\n- README.md, CLAUDE.md: Documentation updates\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(mkdir:*)",
"Bash(ssh:*)"
],
"deny": [],
"ask": []

View File

@@ -0,0 +1,118 @@
name: Bug Report
about: Report a bug or unexpected behavior in ORLY relay
title: "[BUG] "
labels:
- bug
body:
- type: markdown
attributes:
value: |
## Bug Report Guidelines
Thank you for taking the time to report a bug. Please fill out the form below to help us understand and reproduce the issue.
**Before submitting:**
- Search [existing issues](https://git.mleku.dev/mleku/next.orly.dev/issues) to avoid duplicates
- Check the [documentation](https://git.mleku.dev/mleku/next.orly.dev) for configuration guidance
- Ensure you're running a recent version of ORLY
- type: input
id: version
attributes:
label: ORLY Version
description: Run `./orly version` to get the version
placeholder: "v0.35.4"
validations:
required: true
- type: dropdown
id: database
attributes:
label: Database Backend
description: Which database backend are you using?
options:
- Badger (default)
- Neo4j
- WasmDB
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of the bug
placeholder: Describe what happened and what you expected to happen
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Detailed steps to reproduce the behavior
placeholder: |
1. Start relay with `./orly`
2. Connect with client X
3. Perform action Y
4. Observe error Z
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
Include relevant log output. Set `ORLY_LOG_LEVEL=debug` or `trace` for more detail.
This will be automatically formatted as code.
render: shell
- type: textarea
id: config
attributes:
label: Configuration
description: |
Relevant environment variables or configuration (redact sensitive values).
This will be automatically formatted as code.
render: shell
placeholder: |
ORLY_ACL_MODE=follows
ORLY_POLICY_ENABLED=true
ORLY_DB_TYPE=badger
- type: textarea
id: environment
attributes:
label: Environment
description: Operating system, Go version, etc.
placeholder: |
OS: Linux 6.8.0
Go: 1.25.3
Architecture: amd64
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other context, screenshots, or information that might help
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched existing issues and this is not a duplicate
required: true
- label: I have included version information
required: true
- label: I have included steps to reproduce the issue
required: true

View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://git.mleku.dev/mleku/next.orly.dev
about: Check the repository documentation before opening an issue
- name: Nostr Protocol (NIPs)
url: https://github.com/nostr-protocol/nips
about: For questions about Nostr protocol specifications

View File

@@ -0,0 +1,118 @@
name: Feature Request
about: Suggest a new feature or enhancement for ORLY relay
title: "[FEATURE] "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
## Feature Request Guidelines
Thank you for suggesting a feature. Please provide as much detail as possible to help us understand your proposal.
**Before submitting:**
- Search [existing issues](https://git.mleku.dev/mleku/next.orly.dev/issues) to avoid duplicates
- Check if this is covered by an existing [NIP](https://github.com/nostr-protocol/nips)
- Review the [documentation](https://git.mleku.dev/mleku/next.orly.dev) for current capabilities
- type: dropdown
id: category
attributes:
label: Feature Category
description: What area of ORLY does this feature relate to?
options:
- Protocol (NIP implementation)
- Database / Storage
- Performance / Optimization
- Policy / Access Control
- Web UI / Admin Interface
- Deployment / Operations
- API / Integration
- Documentation
- Other
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem Statement
description: |
What problem does this feature solve? Is this related to a frustration you have?
A clear problem statement helps us understand the motivation.
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: |
Describe the solution you'd like. Be specific about expected behavior.
placeholder: "I would like ORLY to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: |
Describe any alternative solutions or workarounds you've considered.
placeholder: "I've tried X but it doesn't work because..."
- type: input
id: nip
attributes:
label: Related NIP
description: If this relates to a Nostr Implementation Possibility, provide the NIP number
placeholder: "NIP-XX"
- type: dropdown
id: impact
attributes:
label: Scope of Impact
description: How significant is this feature?
options:
- Minor enhancement (small quality-of-life improvement)
- Moderate feature (adds useful capability)
- Major feature (significant new functionality)
- Breaking change (requires migration or config changes)
validations:
required: true
- type: dropdown
id: contribution
attributes:
label: Willingness to Contribute
description: Would you be willing to help implement this feature?
options:
- "Yes, I can submit a PR"
- "Yes, I can help with testing"
- "No, but I can provide more details"
- "No"
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: |
Any other context, mockups, examples, or references that help explain the feature.
For protocol features, include example event structures or message flows if applicable.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched existing issues and this is not a duplicate
required: true
- label: I have described the problem this feature solves
required: true
- label: I have checked if this relates to an existing NIP
required: false

53
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Download libsecp256k1
run: |
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O libsecp256k1.so
chmod +x libsecp256k1.so
- name: Run tests
run: |
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(pwd)"
CGO_ENABLED=0 go test ./...
- name: Build binary
run: |
CGO_ENABLED=0 go build -o orly .
./orly version
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Check go mod tidy
run: |
go mod tidy
git diff --exit-code go.mod go.sum
- name: Run go vet
run: CGO_ENABLED=0 go vet ./...

154
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,154 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
platform: linux-amd64
ext: ""
lib: libsecp256k1.so
- goos: linux
goarch: arm64
platform: linux-arm64
ext: ""
lib: libsecp256k1.so
- goos: darwin
goarch: amd64
platform: darwin-amd64
ext: ""
lib: libsecp256k1.dylib
- goos: darwin
goarch: arm64
platform: darwin-arm64
ext: ""
lib: libsecp256k1.dylib
- goos: windows
goarch: amd64
platform: windows-amd64
ext: ".exe"
lib: libsecp256k1.dll
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install bun
run: |
curl -fsSL https://bun.sh/install | bash
echo "$HOME/.bun/bin" >> $GITHUB_PATH
- name: Build Web UI
run: |
cd app/web
$HOME/.bun/bin/bun install
$HOME/.bun/bin/bun run build
- name: Get version
id: version
run: echo "version=$(cat pkg/version/version)" >> $GITHUB_OUTPUT
- name: Build binary
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
VERSION=${{ steps.version.outputs.version }}
OUTPUT="orly-${VERSION}-${{ matrix.platform }}${{ matrix.ext }}"
go build -ldflags "-s -w -X main.version=${VERSION}" -o ${OUTPUT} .
sha256sum ${OUTPUT} > ${OUTPUT}.sha256
- name: Download runtime library
run: |
VERSION=${{ steps.version.outputs.version }}
LIB="${{ matrix.lib }}"
wget -q "https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/${LIB}" -O "${LIB}" || true
if [ -f "${LIB}" ]; then
sha256sum "${LIB}" > "${LIB}.sha256"
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: orly-${{ matrix.platform }}
path: |
orly-*
libsecp256k1*
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get version
id: version
run: echo "version=$(cat pkg/version/version)" >> $GITHUB_OUTPUT
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create combined checksums
run: |
cd artifacts
cat *.sha256 | sort -k2 > SHA256SUMS.txt
rm -f *.sha256
- name: List release files
run: ls -la artifacts/
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: ORLY ${{ steps.version.outputs.version }}
body: |
## ORLY ${{ steps.version.outputs.version }}
### Downloads
Download the appropriate binary for your platform. The `libsecp256k1` library is optional but recommended for better cryptographic performance.
### Installation
1. Download the binary for your platform
2. (Optional) Download the corresponding `libsecp256k1` library
3. Place both files in the same directory
4. Make the binary executable: `chmod +x orly-*`
5. Run: `./orly-*-linux-amd64` (or your platform's binary)
### Verify Downloads
```bash
sha256sum -c SHA256SUMS.txt
```
### Configuration
See the [repository documentation](https://git.mleku.dev/mleku/next.orly.dev) for configuration options.
files: |
artifacts/*
draft: false
prerelease: false

View File

@@ -235,11 +235,18 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
**`pkg/neo4j/`** - Neo4j graph database backend with social graph support
- `neo4j.go` - Main database implementation
- `schema.go` - Graph schema and index definitions (includes WoT extensions)
- `migrations.go` - Database schema migrations (v1: base, v2: WoT, v3: Tag-based e/p)
- `query-events.go` - REQ filter to Cypher translation
- `save-event.go` - Event storage with relationship creation
- `save-event.go` - Event storage with Tag-based relationship creation
- `delete.go` - Event deletion (NIP-09) with Tag traversal for deletion detection
- `social-event-processor.go` - Processes kinds 0, 3, 1984, 10000 for social graph
- `hex_utils.go` - Helpers for binary-to-hex tag value extraction
- `WOT_SPEC.md` - Web of Trust data model specification (NostrUser nodes, trust metrics)
- `MODIFYING_SCHEMA.md` - Guide for schema modifications
- **Tests:**
- `tag_model_test.go` - Tag-based e/p model and filter query tests
- `save-event_test.go` - Event storage and relationship tests
- `social-event-processor_test.go` - Social graph event processing tests
**`pkg/protocol/`** - Nostr protocol implementation
- `ws/` - WebSocket message framing and parsing
@@ -349,6 +356,11 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
- Supports multiple backends via `ORLY_DB_TYPE` environment variable
- **Badger** (default): Embedded key-value store with custom indexing, ideal for single-instance deployments
- **Neo4j**: Graph database with social graph and Web of Trust (WoT) extensions
- **Tag-Based e/p Model**: All tags stored through intermediate Tag nodes
- `Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event` for e-tags
- `Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser` for p-tags
- Enables unified querying: `#e` and `#p` filter queries work correctly
- Automatic migration from direct REFERENCES/MENTIONS (v3 migration)
- Processes kinds 0 (profile), 3 (contacts), 1984 (reports), 10000 (mute list) for social graph
- NostrUser nodes with trust metrics (influence, PageRank)
- FOLLOWS, MUTES, REPORTS relationships for WoT analysis
@@ -816,11 +828,18 @@ The directory spider (`pkg/spider/directory.go`) automatically discovers and syn
### Neo4j Social Graph Backend
The Neo4j backend (`pkg/neo4j/`) includes Web of Trust (WoT) extensions:
- **Tag-Based e/p Model**: All tags (including e/p) stored through intermediate Tag nodes
- `Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event`
- `Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser`
- Enables unified tag querying (`#e` and `#p` filter queries now work)
- v3 migration automatically converts existing direct REFERENCES/MENTIONS
- **Social Event Processor**: Handles kinds 0, 3, 1984, 10000 for social graph management
- **NostrUser nodes**: Store profile data and trust metrics (influence, PageRank)
- **Relationships**: FOLLOWS, MUTES, REPORTS for social graph analysis
- **Deletion Detection**: `CheckForDeleted()` uses Tag traversal for kind 5 event checks
- **WoT Schema**: See `pkg/neo4j/WOT_SPEC.md` for full specification
- **Schema Modifications**: See `pkg/neo4j/MODIFYING_SCHEMA.md` for how to update
- **Comprehensive Tests**: `tag_model_test.go` covers Tag-based model, filter queries, migrations
### WasmDB IndexedDB Backend
WebAssembly-compatible database backend (`pkg/wasmdb/`):

101
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,101 @@
# Contributing to ORLY
Thank you for your interest in contributing to ORLY! This document outlines the process for reporting bugs, requesting features, and submitting contributions.
**Canonical Repository:** https://git.mleku.dev/mleku/next.orly.dev
## Issue Reporting Policy
### Before Opening an Issue
1. **Search existing issues** to avoid duplicates
2. **Check the documentation** in the repository
3. **Verify your version** - run `./orly version` and ensure you're on a recent release
4. **Review the CLAUDE.md** file for configuration guidance
### Bug Reports
Use the **Bug Report** template when reporting unexpected behavior. A good bug report includes:
- **Version information** - exact ORLY version from `./orly version`
- **Database backend** - Badger, Neo4j, or WasmDB
- **Clear description** - what happened vs. what you expected
- **Reproduction steps** - detailed steps to trigger the bug
- **Logs** - relevant log output (use `ORLY_LOG_LEVEL=debug` or `trace`)
- **Configuration** - relevant environment variables (redact secrets)
#### Log Levels for Debugging
```bash
export ORLY_LOG_LEVEL=trace # Most verbose
export ORLY_LOG_LEVEL=debug # Development debugging
export ORLY_LOG_LEVEL=info # Default
```
### Feature Requests
Use the **Feature Request** template when suggesting new functionality. A good feature request includes:
- **Problem statement** - what problem does this solve?
- **Proposed solution** - specific description of desired behavior
- **Alternatives considered** - workarounds you've tried
- **Related NIP** - if this implements a Nostr protocol specification
- **Impact assessment** - is this a minor tweak or major change?
#### Feature Categories
- **Protocol** - NIP implementations and Nostr protocol features
- **Database** - Storage backends, indexing, query optimization
- **Performance** - Caching, SIMD operations, memory optimization
- **Policy** - Access control, event filtering, validation
- **Web UI** - Admin interface improvements
- **Operations** - Deployment, monitoring, systemd integration
## Code Contributions
### Development Setup
```bash
# Clone the repository
git clone https://git.mleku.dev/mleku/next.orly.dev.git
cd next.orly.dev
# Build
CGO_ENABLED=0 go build -o orly
# Run tests
./scripts/test.sh
# Build with web UI
./scripts/update-embedded-web.sh
```
### Pull Request Guidelines
1. **One feature/fix per PR** - keep changes focused
2. **Write tests** - for new functionality and bug fixes
3. **Follow existing patterns** - match the code style of surrounding code
4. **Update documentation** - if your change affects configuration or behavior
5. **Test your changes** - run `./scripts/test.sh` before submitting
### Commit Message Format
```
Short summary (72 chars max, imperative mood)
- Bullet point describing change 1
- Bullet point describing change 2
Files modified:
- path/to/file1.go: Description of change
- path/to/file2.go: Description of change
```
## Communication
- **Issues:** https://git.mleku.dev/mleku/next.orly.dev/issues
- **Documentation:** https://git.mleku.dev/mleku/next.orly.dev
## License
By contributing to ORLY, you agree that your contributions will be licensed under the same license as the project.

View File

@@ -0,0 +1,325 @@
# ORLY Expansion Plan: Documentation, Installer, Tray, and WireGuard
## Overview
Expand ORLY from a relay binary into a complete ecosystem for personal Nostr relay deployment, with:
1. **Textbook-style README** - Progressive documentation from novice to expert
2. **GUI Installer** - Wails-based setup wizard (Linux + macOS)
3. **System Tray** - Service monitoring and control
4. **WireGuard Client** - Embedded tunnel for NAT traversal
5. **Proxy Server** - Self-hostable AND managed service option
---
## Architecture
```
USER SYSTEMS
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ orly-setup │ │ orly │ │ orly --tray │ │
│ │ (Installer) │ │ (Relay) │ │ (Systray) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ generates │ serves │ monitors │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ~/.config/ │ │ :3334 WS/HTTP│ │ /api/admin/* │ │
│ │ systemd svc │ │ + WG tunnel │ │ status/ctrl │ │
│ └──────────────┘ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ pkg/tunnel/ │ │
│ │ WireGuard │ │
│ └───────┬───────┘ │
└─────────────────────────────┼───────────────────────────────────────┘
│ WG Tunnel (UDP :51820)
┌─────────────────────────────────────────────────────────────────────┐
│ PROXY SERVER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WG Server │───▶│ Nostr Auth │───▶│ Public Proxy │ │
│ │ :51820 │ │ (npub-based) │ │ Egress │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Package Structure
```
next.orly.dev/
├── cmd/
│ ├── orly-setup/ # NEW: Wails installer
│ │ ├── main.go
│ │ ├── app.go # Backend logic
│ │ ├── frontend/ # Svelte wizard UI
│ │ │ └── src/steps/ # Welcome, Config, Install, Complete
│ │ └── install/
│ │ ├── preflight.go # Dependency checks
│ │ ├── systemd.go # Service creation
│ │ └── verify.go # Post-install checks
│ │
│ └── proxy-server/ # NEW: WireGuard proxy
│ ├── main.go
│ ├── server.go # WG server
│ ├── auth.go # Nostr auth
│ └── registry.go # User management
├── pkg/
│ ├── tunnel/ # NEW: Embedded WG client
│ │ ├── tunnel.go # Main interface
│ │ ├── client.go # wireguard-go wrapper
│ │ ├── reconnect.go # Auto-reconnect
│ │ └── health.go # Connection health
│ │
│ ├── tray/ # NEW: System tray
│ │ ├── tray.go # Platform abstraction
│ │ ├── tray_linux.go # Linux implementation
│ │ ├── tray_darwin.go # macOS implementation
│ │ └── menu.go # Menu construction
│ │
│ ├── admin/ # NEW: Admin HTTP API
│ │ ├── api.go # Router
│ │ ├── status.go # GET /api/admin/status
│ │ ├── control.go # POST /api/admin/start|stop|restart
│ │ └── logs.go # GET /api/admin/logs (SSE)
│ │
│ └── interfaces/
│ ├── tunnel/tunnel.go # Tunnel interface
│ ├── tray/tray.go # Tray interface
│ └── admin/admin.go # Admin API interface
└── docs/
└── README.adoc # NEW: Textbook-style docs
```
---
## Implementation Phases
### Phase 1: Documentation Foundation
**Files to create/modify:**
- `README.adoc` - New textbook-style documentation
- `docs/` - Reorganize scattered docs
**README Structure (Textbook Style):**
```
Chapter 1: Quick Start (5-minute setup)
Chapter 2: Installation (platform-specific)
Chapter 3: Configuration (all env vars)
Chapter 4: Operations (systemd, monitoring)
Chapter 5: Security (TLS, ACLs, policy)
Chapter 6: Advanced (Neo4j, clustering, WoT)
Chapter 7: Architecture (internals)
Appendices: Reference tables, troubleshooting
```
### Phase 2: Admin API
**Files to create:**
- `pkg/admin/api.go` - Router and middleware
- `pkg/admin/status.go` - Status endpoint
- `pkg/admin/control.go` - Start/stop/restart
- `pkg/admin/logs.go` - Log streaming via SSE
- `pkg/interfaces/admin/admin.go` - Interface definition
**Files to modify:**
- `app/server.go` - Register `/api/admin/*` routes
- `app/config/config.go` - Add admin API config
**Endpoints:**
```
GET /api/admin/status - Relay status, uptime, connections
POST /api/admin/start - Start relay (when in tray mode)
POST /api/admin/stop - Graceful shutdown
POST /api/admin/restart - Graceful restart
GET /api/admin/logs - SSE log stream
```
### Phase 3: System Tray
**Files to create:**
- `pkg/tray/tray.go` - Platform abstraction
- `pkg/tray/tray_linux.go` - Linux (dbus/appindicator)
- `pkg/tray/tray_darwin.go` - macOS (NSStatusBar)
- `pkg/tray/menu.go` - Menu construction
- `pkg/interfaces/tray/tray.go` - Interface
**Files to modify:**
- `main.go` - Add `--tray` flag handling
- `app/config/config.go` - Add tray config
**Features:**
- Status icon (green/yellow/red)
- Start/Stop/Restart menu items
- Open Web UI (launches browser)
- View Logs submenu
- Auto-start on login toggle
### Phase 4: Installer GUI (Wails)
**Files to create:**
- `cmd/orly-setup/main.go` - Wails entry point
- `cmd/orly-setup/app.go` - Backend methods
- `cmd/orly-setup/frontend/` - Svelte wizard
- `cmd/orly-setup/install/preflight.go` - Dependency checks
- `cmd/orly-setup/install/systemd.go` - Service creation
- `cmd/orly-setup/install/config.go` - Config generation
- `cmd/orly-setup/install/verify.go` - Post-install checks
- `scripts/build-installer.sh` - Build script
**Wizard Steps:**
1. Welcome - Introduction, license
2. Preflight - Check Go, disk, ports
3. Configuration - Port, data dir, TLS domains
4. Admin Setup - Generate or import admin keys
5. Database - Choose Badger or Neo4j
6. WireGuard (optional) - Tunnel config
7. Installation - Create service, start relay
8. Complete - Verify and show status
### Phase 5: WireGuard Client
**Files to create:**
- `pkg/tunnel/tunnel.go` - Main interface
- `pkg/tunnel/client.go` - wireguard-go wrapper
- `pkg/tunnel/config.go` - WG configuration
- `pkg/tunnel/reconnect.go` - Auto-reconnect logic
- `pkg/tunnel/health.go` - Health monitoring
- `pkg/tunnel/handoff.go` - Graceful restart
- `pkg/interfaces/tunnel/tunnel.go` - Interface
**Files to modify:**
- `app/config/config.go` - Add WG config fields
- `app/main.go` - Initialize tunnel on startup
- `main.go` - Tunnel lifecycle management
**Config additions:**
```go
WGEnabled bool `env:"ORLY_WG_ENABLED" default:"false"`
WGServer string `env:"ORLY_WG_SERVER"`
WGPrivateKey string `env:"ORLY_WG_PRIVATE_KEY"`
WGServerPubKey string `env:"ORLY_WG_PUBLIC_KEY"`
WGKeepalive int `env:"ORLY_WG_KEEPALIVE" default:"25"`
WGMTU int `env:"ORLY_WG_MTU" default:"1280"`
WGReconnect bool `env:"ORLY_WG_RECONNECT" default:"true"`
```
### Phase 6: Proxy Server
**Files to create:**
- `cmd/proxy-server/main.go` - Entry point
- `cmd/proxy-server/server.go` - WG server management
- `cmd/proxy-server/auth.go` - Nostr-based auth
- `cmd/proxy-server/registry.go` - User/relay registry
- `cmd/proxy-server/bandwidth.go` - Traffic monitoring
- `cmd/proxy-server/config.go` - Server configuration
**Features:**
- WireGuard server (wireguard-go)
- Nostr event-based authentication (NIP-98 style)
- User registration via signed events
- Relay discovery and assignment
- Bandwidth monitoring and quotas
- Multi-tenant isolation
---
## Key Interfaces
### Tunnel Interface
```go
type Tunnel interface {
Connect(ctx context.Context) error
Disconnect() error
Status() TunnelStatus
Handoff() (*HandoffState, error)
Resume(state *HandoffState) error
}
```
### Admin API Interface
```go
type AdminAPI interface {
Status() (*RelayStatus, error)
Start() error
Stop() error
Restart() error
Logs(ctx context.Context, lines int) (<-chan LogEntry, error)
}
```
### Tray Interface
```go
type TrayApp interface {
Run() error
Quit()
UpdateStatus(status StatusLevel, tooltip string)
ShowNotification(title, message string)
}
```
---
## Dependencies to Add
```go
// go.mod additions
require (
github.com/wailsapp/wails/v2 v2.x.x // Installer GUI
golang.zx2c4.com/wireguard v0.x.x // WireGuard client
github.com/getlantern/systray v1.x.x // System tray (or fyne.io/systray)
)
```
---
## Build Commands
```bash
# Standard relay build (unchanged)
CGO_ENABLED=0 go build -o orly
# Relay with tray support
CGO_ENABLED=0 go build -tags tray -o orly
# Installer GUI
cd cmd/orly-setup && wails build -platform linux/amd64,darwin/amd64
# Proxy server
CGO_ENABLED=0 go build -o orly-proxy ./cmd/proxy-server
# All platforms
./scripts/build-all.sh
```
---
## Critical Files Reference
| File | Purpose |
|------|---------|
| `app/config/config.go` | Add WG, tray, admin API config |
| `app/server.go` | Register admin API routes |
| `main.go` | Add --tray flag, WG initialization |
| `scripts/deploy.sh` | Pattern for installer service creation |
| `app/web/src/App.svelte` | Pattern for installer UI |
---
## Backward Compatibility
- Main `orly` binary behavior unchanged without flags
- All new features opt-in via environment variables
- WireGuard gracefully degrades if connection fails
- Tray mode only activates with `--tray` flag
- Admin API can be disabled via `ORLY_ADMIN_API_ENABLED=false`
---
## Success Criteria
1. New user can install via GUI wizard in < 5 minutes
2. README guides user from zero to running relay
3. System tray provides one-click relay management
4. WireGuard tunnel auto-connects and reconnects
5. Proxy server enables home relay exposure without port forwarding
6. All existing functionality preserved

View File

@@ -35,10 +35,12 @@ export ORLY_NEO4J_PASSWORD=password
## Features
- **Graph-Native Storage**: Events, authors, and tags stored as nodes and relationships
- **Unified Tag Model**: All tags (including e/p tags) stored as Tag nodes with REFERENCES relationships
- **Efficient Queries**: Leverages Neo4j's native graph traversal for tag and social graph queries
- **Cypher Query Language**: Powerful, expressive query language for complex filters
- **Automatic Indexing**: Unique constraints and indexes for optimal performance
- **Relationship Queries**: Native support for event references, mentions, and tags
- **Automatic Migrations**: Schema migrations run automatically on startup
- **Web of Trust (WoT) Extensions**: Optional support for trust metrics, social graph analysis, and content filtering (see [WOT_SPEC.md](./WOT_SPEC.md))
## Architecture
@@ -50,6 +52,23 @@ See [docs/NEO4J_BACKEND.md](../../docs/NEO4J_BACKEND.md) for comprehensive docum
- Development guide
- Comparison with other backends
### Tag-Based e/p Model
All tags, including `e` (event references) and `p` (pubkey mentions), are stored through intermediate Tag nodes:
```
Event -[:TAGGED_WITH]-> Tag{type:'e',value:eventId} -[:REFERENCES]-> Event
Event -[:TAGGED_WITH]-> Tag{type:'p',value:pubkey} -[:REFERENCES]-> NostrUser
Event -[:TAGGED_WITH]-> Tag{type:'t',value:topic} (no REFERENCES for regular tags)
```
**Benefits:**
- Unified tag querying: `#e` and `#p` filter queries work correctly
- Consistent data model: All tags use the same TAGGED_WITH pattern
- Graph traversal: Can traverse from events through tags to referenced entities
**Migration:** Existing databases with direct `REFERENCES`/`MENTIONS` relationships are automatically migrated at startup via v3 migration.
### Web of Trust (WoT) Extensions
This package includes schema support for Web of Trust trust metrics computation:
@@ -96,6 +115,8 @@ This package includes schema support for Web of Trust trust metrics computation:
### Tests
- `social-event-processor_test.go` - Comprehensive tests for kinds 0, 3, 1984, 10000
- `tag_model_test.go` - Tag-based e/p model tests and filter query tests
- `save-event_test.go` - Event storage and relationship tests
## Testing
@@ -166,11 +187,25 @@ MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: "t", value: "bitcoin"})
RETURN e
```
### Event reference query (e-tags)
```cypher
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: "e"})-[:REFERENCES]->(ref:Event)
WHERE e.id = "abc123..."
RETURN e, ref
```
### Mentions query (p-tags)
```cypher
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: "p"})-[:REFERENCES]->(u:NostrUser)
WHERE e.id = "abc123..."
RETURN e, u
```
### Social graph query
```cypher
MATCH (author:NostrUser {pubkey: "abc123..."})
<-[:AUTHORED_BY]-(e:Event)
-[:MENTIONS]->(mentioned:NostrUser)
-[:TAGGED_WITH]->(:Tag {type: "p"})-[:REFERENCES]->(mentioned:NostrUser)
RETURN author, e, mentioned
```

View File

@@ -125,6 +125,40 @@ Legacy node label that is redundant with SetOfNostrUserWotMetricsCards. Should b
### Relationship Types
#### Tag-Based References (e and p tags)
The Neo4j backend uses a unified Tag-based model for `e` and `p` tags, enabling consistent tag querying while maintaining graph traversal capabilities.
**E-tags (Event References):**
```
(Event)-[:TAGGED_WITH]->(Tag {type: 'e', value: <event_id>})-[:REFERENCES]->(Event)
```
**P-tags (Pubkey Mentions):**
```
(Event)-[:TAGGED_WITH]->(Tag {type: 'p', value: <pubkey>})-[:REFERENCES]->(NostrUser)
```
This model provides:
- Unified tag querying via `#e` and `#p` filters (same as other tags)
- Graph traversal from events to referenced events/users
- Consistent indexing through existing Tag node indexes
**Query Examples:**
```cypher
-- Find all events that reference a specific event
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $eventId})-[:REFERENCES]->(ref:Event)
RETURN e
-- Find all events that mention a specific pubkey
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $pubkey})-[:REFERENCES]->(u:NostrUser)
RETURN e
-- Count references to an event (thread replies)
MATCH (t:Tag {type: 'e', value: $eventId})<-[:TAGGED_WITH]-(e:Event)
RETURN count(e) AS replyCount
```
#### 1. FOLLOWS
Represents a follow relationship between users (derived from kind 3 events).
@@ -247,8 +281,9 @@ Comprehensive implementation with additional features:
- `IS_A_REACTION_TO` (kind 7 reactions)
- `IS_A_RESPONSE_TO` (kind 1 replies)
- `IS_A_REPOST_OF` (kind 6, kind 16 reposts)
- `P_TAGGED` (p-tag mentions from events to users)
- `E_TAGGED` (e-tag references from events to events)
- Tag-based references (see "Tag-Based References" section above):
- `Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser` (p-tag mentions)
- `Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event` (e-tag references)
- NostrRelay, CashuMint nodes for ecosystem mapping
- Enhanced GrapeRank incorporating zaps, replies, reactions

View File

@@ -175,14 +175,15 @@ func (n *N) ProcessDelete(ev *event.E, admins [][]byte) error {
// CheckForDeleted checks if an event has been deleted
func (n *N) CheckForDeleted(ev *event.E, admins [][]byte) error {
// Query for kind 5 events that reference this event
// Query for kind 5 events that reference this event via Tag nodes
ctx := context.Background()
idStr := hex.Enc(ev.ID[:])
// Build cypher query to find deletion events
// Traverses through Tag nodes: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event
cypher := `
MATCH (target:Event {id: $targetId})
MATCH (delete:Event {kind: 5})-[:REFERENCES]->(target)
MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target)
WHERE delete.pubkey = $pubkey OR delete.pubkey IN $admins
RETURN delete.id AS id
LIMIT 1`

View File

@@ -25,6 +25,11 @@ var migrations = []Migration{
Description: "Clean up binary-encoded pubkeys and event IDs to lowercase hex",
Migrate: migrateBinaryToHex,
},
{
Version: "v3",
Description: "Convert direct REFERENCES/MENTIONS relationships to Tag-based model",
Migrate: migrateToTagBasedReferences,
},
}
// RunMigrations executes all pending migrations
@@ -343,3 +348,147 @@ func migrateBinaryToHex(ctx context.Context, n *N) error {
n.Logger.Infof("binary-to-hex migration completed successfully")
return nil
}
// migrateToTagBasedReferences converts direct REFERENCES and MENTIONS relationships
// to the new Tag-based model where:
// - Event-[:REFERENCES]->Event becomes Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event
// - Event-[:MENTIONS]->NostrUser becomes Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->NostrUser
//
// This enables unified tag querying via #e and #p filters while maintaining graph traversal.
func migrateToTagBasedReferences(ctx context.Context, n *N) error {
// Step 1: Count existing direct REFERENCES relationships (Event->Event)
countRefCypher := `
MATCH (source:Event)-[r:REFERENCES]->(target:Event)
RETURN count(r) AS count
`
result, err := n.ExecuteRead(ctx, countRefCypher, nil)
if err != nil {
return fmt.Errorf("failed to count REFERENCES relationships: %w", err)
}
var refCount int64
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
refCount = count
}
}
n.Logger.Infof("found %d direct Event-[:REFERENCES]->Event relationships to migrate", refCount)
// Step 2: Count existing direct MENTIONS relationships (Event->NostrUser)
countMentionsCypher := `
MATCH (source:Event)-[r:MENTIONS]->(target:NostrUser)
RETURN count(r) AS count
`
result, err = n.ExecuteRead(ctx, countMentionsCypher, nil)
if err != nil {
return fmt.Errorf("failed to count MENTIONS relationships: %w", err)
}
var mentionsCount int64
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
mentionsCount = count
}
}
n.Logger.Infof("found %d direct Event-[:MENTIONS]->NostrUser relationships to migrate", mentionsCount)
// If nothing to migrate, we're done
if refCount == 0 && mentionsCount == 0 {
n.Logger.Infof("no direct relationships to migrate, migration complete")
return nil
}
// Step 3: Migrate REFERENCES relationships to Tag-based model
// Process in batches to avoid memory issues with large datasets
if refCount > 0 {
n.Logger.Infof("migrating %d REFERENCES relationships to Tag-based model...", refCount)
// This query:
// 1. Finds Event->Event REFERENCES relationships
// 2. Creates/merges Tag node with type='e' and value=target event ID
// 3. Creates TAGGED_WITH from source Event to Tag
// 4. Creates REFERENCES from Tag to target Event
// 5. Deletes the old direct REFERENCES relationship
migrateRefCypher := `
MATCH (source:Event)-[r:REFERENCES]->(target:Event)
WITH source, r, target LIMIT 1000
MERGE (t:Tag {type: 'e', value: target.id})
CREATE (source)-[:TAGGED_WITH]->(t)
MERGE (t)-[:REFERENCES]->(target)
DELETE r
RETURN count(r) AS migrated
`
// Run migration in batches until no more relationships exist
totalMigrated := int64(0)
for {
result, err := n.ExecuteWrite(ctx, migrateRefCypher, nil)
if err != nil {
return fmt.Errorf("failed to migrate REFERENCES batch: %w", err)
}
var batchMigrated int64
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
batchMigrated = count
}
}
if batchMigrated == 0 {
break
}
totalMigrated += batchMigrated
n.Logger.Infof("migrated %d REFERENCES relationships (total: %d)", batchMigrated, totalMigrated)
}
n.Logger.Infof("completed migrating %d REFERENCES relationships", totalMigrated)
}
// Step 4: Migrate MENTIONS relationships to Tag-based model
if mentionsCount > 0 {
n.Logger.Infof("migrating %d MENTIONS relationships to Tag-based model...", mentionsCount)
// This query:
// 1. Finds Event->NostrUser MENTIONS relationships
// 2. Creates/merges Tag node with type='p' and value=target pubkey
// 3. Creates TAGGED_WITH from source Event to Tag
// 4. Creates REFERENCES from Tag to target NostrUser
// 5. Deletes the old direct MENTIONS relationship
migrateMentionsCypher := `
MATCH (source:Event)-[r:MENTIONS]->(target:NostrUser)
WITH source, r, target LIMIT 1000
MERGE (t:Tag {type: 'p', value: target.pubkey})
CREATE (source)-[:TAGGED_WITH]->(t)
MERGE (t)-[:REFERENCES]->(target)
DELETE r
RETURN count(r) AS migrated
`
// Run migration in batches until no more relationships exist
totalMigrated := int64(0)
for {
result, err := n.ExecuteWrite(ctx, migrateMentionsCypher, nil)
if err != nil {
return fmt.Errorf("failed to migrate MENTIONS batch: %w", err)
}
var batchMigrated int64
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
batchMigrated = count
}
}
if batchMigrated == 0 {
break
}
totalMigrated += batchMigrated
n.Logger.Infof("migrated %d MENTIONS relationships (total: %d)", batchMigrated, totalMigrated)
}
n.Logger.Infof("completed migrating %d MENTIONS relationships", totalMigrated)
}
n.Logger.Infof("Tag-based references migration completed successfully")
return nil
}

View File

@@ -238,7 +238,8 @@ func (n *N) addTagsInBatches(c context.Context, eventID string, ev *event.E) err
}
// addPTagsInBatches adds p-tag (pubkey mention) relationships using UNWIND for efficiency.
// Creates NostrUser nodes for mentioned pubkeys and MENTIONS relationships.
// Creates Tag nodes with type='p' and REFERENCES relationships to NostrUser nodes.
// This enables unified tag querying via #p filters while maintaining the social graph.
func (n *N) addPTagsInBatches(c context.Context, eventID string, pTags []string) error {
// Process in batches to avoid memory issues
for i := 0; i < len(pTags); i += tagBatchSize {
@@ -249,12 +250,17 @@ func (n *N) addPTagsInBatches(c context.Context, eventID string, pTags []string)
batch := pTags[i:end]
// Use UNWIND to process multiple p-tags in a single query
// Creates Tag nodes as intermediaries, enabling unified #p filter queries
// Tag-[:REFERENCES]->NostrUser allows graph traversal from tag to user
cypher := `
MATCH (e:Event {id: $eventId})
UNWIND $pubkeys AS pubkey
MERGE (t:Tag {type: 'p', value: pubkey})
CREATE (e)-[:TAGGED_WITH]->(t)
WITH t, pubkey
MERGE (u:NostrUser {pubkey: pubkey})
ON CREATE SET u.created_at = timestamp()
CREATE (e)-[:MENTIONS]->(u)`
MERGE (t)-[:REFERENCES]->(u)`
params := map[string]any{
"eventId": eventID,
@@ -270,7 +276,8 @@ CREATE (e)-[:MENTIONS]->(u)`
}
// addETagsInBatches adds e-tag (event reference) relationships using UNWIND for efficiency.
// Only creates REFERENCES relationships if the referenced event exists.
// Creates Tag nodes with type='e' and REFERENCES relationships to Event nodes (if they exist).
// This enables unified tag querying via #e filters while maintaining event graph structure.
func (n *N) addETagsInBatches(c context.Context, eventID string, eTags []string) error {
// Process in batches to avoid memory issues
for i := 0; i < len(eTags); i += tagBatchSize {
@@ -281,14 +288,18 @@ func (n *N) addETagsInBatches(c context.Context, eventID string, eTags []string)
batch := eTags[i:end]
// Use UNWIND to process multiple e-tags in a single query
// OPTIONAL MATCH ensures we only create relationships if referenced event exists
// Creates Tag nodes as intermediaries, enabling unified #e filter queries
// Tag-[:REFERENCES]->Event allows graph traversal from tag to referenced event
// OPTIONAL MATCH ensures we only create REFERENCES if referenced event exists
cypher := `
MATCH (e:Event {id: $eventId})
UNWIND $eventIds AS refId
MERGE (t:Tag {type: 'e', value: refId})
CREATE (e)-[:TAGGED_WITH]->(t)
WITH t, refId
OPTIONAL MATCH (ref:Event {id: refId})
WITH e, ref
WHERE ref IS NOT NULL
CREATE (e)-[:REFERENCES]->(ref)`
MERGE (t)-[:REFERENCES]->(ref)`
params := map[string]any{
"eventId": eventID,

View File

@@ -151,7 +151,7 @@ func TestSafePrefix(t *testing.T) {
}
// TestSaveEvent_ETagReference tests that events with e-tags are saved correctly
// and the REFERENCES relationships are created when the referenced event exists.
// using the Tag-based model: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event.
// Uses shared testDB from testmain_test.go to avoid auth rate limiting.
func TestSaveEvent_ETagReference(t *testing.T) {
if testDB == nil {
@@ -226,10 +226,10 @@ func TestSaveEvent_ETagReference(t *testing.T) {
t.Fatal("Reply event should not exist yet")
}
// Verify REFERENCES relationship was created
// Verify Tag-based e-tag model: Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event
cypher := `
MATCH (reply:Event {id: $replyId})-[:REFERENCES]->(root:Event {id: $rootId})
RETURN reply.id AS replyId, root.id AS rootId
MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $rootId})-[:REFERENCES]->(root:Event {id: $rootId})
RETURN reply.id AS replyId, t.value AS tagValue, root.id AS rootId
`
params := map[string]any{
"replyId": hex.Enc(replyEvent.ID[:]),
@@ -238,42 +238,43 @@ func TestSaveEvent_ETagReference(t *testing.T) {
result, err := testDB.ExecuteRead(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to query REFERENCES relationship: %v", err)
t.Fatalf("Failed to query Tag-based REFERENCES: %v", err)
}
if !result.Next(ctx) {
t.Error("Expected REFERENCES relationship between reply and root events")
t.Error("Expected Tag-based REFERENCES relationship between reply and root events")
} else {
record := result.Record()
returnedReplyId := record.Values[0].(string)
returnedRootId := record.Values[1].(string)
t.Logf("✓ REFERENCES relationship verified: %s -> %s", returnedReplyId[:8], returnedRootId[:8])
tagValue := record.Values[1].(string)
returnedRootId := record.Values[2].(string)
t.Logf("✓ Tag-based REFERENCES verified: Event(%s) -> Tag{e:%s} -> Event(%s)", returnedReplyId[:8], tagValue[:8], returnedRootId[:8])
}
// Verify MENTIONS relationship was also created for the p-tag
mentionsCypher := `
MATCH (reply:Event {id: $replyId})-[:MENTIONS]->(author:NostrUser {pubkey: $authorPubkey})
RETURN author.pubkey AS pubkey
// Verify Tag-based p-tag model: Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser
pTagCypher := `
MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $authorPubkey})-[:REFERENCES]->(author:NostrUser {pubkey: $authorPubkey})
RETURN author.pubkey AS pubkey, t.value AS tagValue
`
mentionsParams := map[string]any{
pTagParams := map[string]any{
"replyId": hex.Enc(replyEvent.ID[:]),
"authorPubkey": hex.Enc(alice.Pub()),
}
mentionsResult, err := testDB.ExecuteRead(ctx, mentionsCypher, mentionsParams)
pTagResult, err := testDB.ExecuteRead(ctx, pTagCypher, pTagParams)
if err != nil {
t.Fatalf("Failed to query MENTIONS relationship: %v", err)
t.Fatalf("Failed to query Tag-based p-tag: %v", err)
}
if !mentionsResult.Next(ctx) {
t.Error("Expected MENTIONS relationship for p-tag")
if !pTagResult.Next(ctx) {
t.Error("Expected Tag-based p-tag relationship")
} else {
t.Logf("✓ MENTIONS relationship verified")
t.Logf("✓ Tag-based p-tag relationship verified")
}
}
// TestSaveEvent_ETagMissingReference tests that e-tags to non-existent events
// don't create broken relationships (batched processing handles this gracefully).
// create Tag nodes but don't create REFERENCES relationships to missing events.
// Uses shared testDB from testmain_test.go to avoid auth rate limiting.
func TestSaveEvent_ETagMissingReference(t *testing.T) {
if testDB == nil {
@@ -331,29 +332,50 @@ func TestSaveEvent_ETagMissingReference(t *testing.T) {
t.Error("Event should have been saved despite missing reference")
}
// Verify no REFERENCES relationship was created (as the target doesn't exist)
// Verify Tag node was created with TAGGED_WITH relationship
tagCypher := `
MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $refId})
RETURN t.value AS tagValue
`
tagParams := map[string]any{
"eventId": hex.Enc(ev.ID[:]),
"refId": nonExistentEventID,
}
tagResult, err := testDB.ExecuteRead(ctx, tagCypher, tagParams)
if err != nil {
t.Fatalf("Failed to check Tag node: %v", err)
}
if !tagResult.Next(ctx) {
t.Error("Expected Tag node to be created for e-tag even when target doesn't exist")
} else {
t.Logf("✓ Tag node created for missing reference")
}
// Verify no REFERENCES relationship was created from Tag (as the target Event doesn't exist)
refCypher := `
MATCH (e:Event {id: $eventId})-[:REFERENCES]->(ref:Event)
MATCH (t:Tag {type: 'e', value: $refId})-[:REFERENCES]->(ref:Event)
RETURN count(ref) AS refCount
`
refParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
refParams := map[string]any{"refId": nonExistentEventID}
refResult, err := testDB.ExecuteRead(ctx, refCypher, refParams)
if err != nil {
t.Fatalf("Failed to check references: %v", err)
t.Fatalf("Failed to check REFERENCES from Tag: %v", err)
}
if refResult.Next(ctx) {
count := refResult.Record().Values[0].(int64)
if count > 0 {
t.Errorf("Expected no REFERENCES relationship for non-existent event, got %d", count)
t.Errorf("Expected no REFERENCES from Tag for non-existent event, got %d", count)
} else {
t.Logf("✓ Correctly handled missing reference (no relationship created)")
t.Logf("✓ Correctly handled missing reference (no REFERENCES from Tag)")
}
}
}
// TestSaveEvent_MultipleETags tests events with multiple e-tags.
// TestSaveEvent_MultipleETags tests events with multiple e-tags using Tag-based model.
// Uses shared testDB from testmain_test.go to avoid auth rate limiting.
func TestSaveEvent_MultipleETags(t *testing.T) {
if testDB == nil {
@@ -409,7 +431,7 @@ func TestSaveEvent_MultipleETags(t *testing.T) {
t.Fatalf("Failed to sign reply event: %v", err)
}
// Save reply event - tests batched e-tag creation
// Save reply event - tests batched e-tag creation with Tag nodes
exists, err := testDB.SaveEvent(ctx, replyEvent)
if err != nil {
t.Fatalf("Failed to save multi-reference event: %v", err)
@@ -418,16 +440,17 @@ func TestSaveEvent_MultipleETags(t *testing.T) {
t.Fatal("Reply event should not exist yet")
}
// Verify all REFERENCES relationships were created
// Verify all Tag-based REFERENCES relationships were created
// Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event
cypher := `
MATCH (reply:Event {id: $replyId})-[:REFERENCES]->(ref:Event)
MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(ref:Event)
RETURN ref.id AS refId
`
params := map[string]any{"replyId": hex.Enc(replyEvent.ID[:])}
result, err := testDB.ExecuteRead(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to query REFERENCES relationships: %v", err)
t.Fatalf("Failed to query Tag-based REFERENCES: %v", err)
}
referencedIDs := make(map[string]bool)
@@ -437,20 +460,20 @@ func TestSaveEvent_MultipleETags(t *testing.T) {
}
if len(referencedIDs) != 3 {
t.Errorf("Expected 3 REFERENCES relationships, got %d", len(referencedIDs))
t.Errorf("Expected 3 Tag-based REFERENCES, got %d", len(referencedIDs))
}
for i, id := range eventIDs {
if !referencedIDs[id] {
t.Errorf("Missing REFERENCES relationship to event %d (%s)", i, id[:8])
t.Errorf("Missing Tag-based REFERENCES to event %d (%s)", i, id[:8])
}
}
t.Logf("✓ All %d REFERENCES relationships created successfully", len(referencedIDs))
t.Logf("✓ All %d Tag-based REFERENCES created successfully", len(referencedIDs))
}
// TestSaveEvent_LargePTagBatch tests that events with many p-tags are saved correctly
// using batched processing to avoid Neo4j stack overflow.
// using batched Tag-based processing to avoid Neo4j stack overflow.
// Uses shared testDB from testmain_test.go to avoid auth rate limiting.
func TestSaveEvent_LargePTagBatch(t *testing.T) {
if testDB == nil {
@@ -498,24 +521,45 @@ func TestSaveEvent_LargePTagBatch(t *testing.T) {
t.Fatal("Event should not exist yet")
}
// Verify all MENTIONS relationships were created
countCypher := `
MATCH (e:Event {id: $eventId})-[:MENTIONS]->(u:NostrUser)
RETURN count(u) AS mentionCount
// Verify all Tag nodes were created with TAGGED_WITH relationships
tagCountCypher := `
MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})
RETURN count(t) AS tagCount
`
countParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
tagCountParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
result, err := testDB.ExecuteRead(ctx, countCypher, countParams)
tagResult, err := testDB.ExecuteRead(ctx, tagCountCypher, tagCountParams)
if err != nil {
t.Fatalf("Failed to count MENTIONS: %v", err)
t.Fatalf("Failed to count p-tag Tag nodes: %v", err)
}
if result.Next(ctx) {
count := result.Record().Values[0].(int64)
if tagResult.Next(ctx) {
count := tagResult.Record().Values[0].(int64)
if count != int64(numTags) {
t.Errorf("Expected %d MENTIONS relationships, got %d", numTags, count)
t.Errorf("Expected %d Tag nodes, got %d", numTags, count)
} else {
t.Logf("✓ All %d MENTIONS relationships created via batched processing", count)
t.Logf("✓ All %d p-tag Tag nodes created via batched processing", count)
}
}
// Verify all REFERENCES relationships to NostrUser were created
refCountCypher := `
MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})-[:REFERENCES]->(u:NostrUser)
RETURN count(u) AS refCount
`
refCountParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
refResult, err := testDB.ExecuteRead(ctx, refCountCypher, refCountParams)
if err != nil {
t.Fatalf("Failed to count Tag-based REFERENCES to NostrUser: %v", err)
}
if refResult.Next(ctx) {
count := refResult.Record().Values[0].(int64)
if count != int64(numTags) {
t.Errorf("Expected %d REFERENCES to NostrUser, got %d", numTags, count)
} else {
t.Logf("✓ All %d Tag-based REFERENCES to NostrUser created via batched processing", count)
}
}
}

1105
pkg/neo4j/tag_model_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
v0.35.3
v0.36.0