upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 14:31:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 15:22:38 +0000
commitd2ac69816567f092fe0d4661723bc43778cb481b (patch)
treee8b51b61a6a7b0ab1a214adebe4e237143b01f0b /src
parent7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (diff)
fix cargo clippy and fmt warnings
Diffstat (limited to 'src')
-rw-r--r--src/config.rs8
-rw-r--r--src/git/handlers.rs1
-rw-r--r--src/git/mod.rs13
-rw-r--r--src/git/protocol.rs18
-rw-r--r--src/git/subprocess.rs34
-rw-r--r--src/http/mod.rs125
-rw-r--r--src/http/nip11.rs34
-rw-r--r--src/nostr/builder.rs14
-rw-r--r--src/nostr/events.rs18
9 files changed, 140 insertions, 125 deletions
diff --git a/src/config.rs b/src/config.rs
index f04b7d8..9b0d0b8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -5,8 +5,10 @@ use std::env;
5/// Database backend type for the relay 5/// Database backend type for the relay
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")] 7#[serde(rename_all = "lowercase")]
8#[derive(Default)]
8pub enum DatabaseBackend { 9pub enum DatabaseBackend {
9 /// In-memory database (default, fastest, no persistence) 10 /// In-memory database (default, fastest, no persistence)
11 #[default]
10 Memory, 12 Memory,
11 /// NostrDB backend (persistent, optimized for Nostr) 13 /// NostrDB backend (persistent, optimized for Nostr)
12 NostrDb, 14 NostrDb,
@@ -14,12 +16,6 @@ pub enum DatabaseBackend {
14 Lmdb, 16 Lmdb,
15} 17}
16 18
17impl Default for DatabaseBackend {
18 fn default() -> Self {
19 Self::Memory
20 }
21}
22
23impl std::str::FromStr for DatabaseBackend { 19impl std::str::FromStr for DatabaseBackend {
24 type Err = anyhow::Error; 20 type Err = anyhow::Error;
25 21
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 00f2449..e84cabb 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -5,7 +5,6 @@
5use http_body_util::Full; 5use http_body_util::Full;
6use hyper::{body::Bytes, Response, StatusCode}; 6use hyper::{body::Bytes, Response, StatusCode};
7use nostr_relay_builder::prelude::MemoryDatabase; 7use nostr_relay_builder::prelude::MemoryDatabase;
8use nostr_sdk::EventId;
9use std::path::PathBuf; 8use std::path::PathBuf;
10use std::sync::Arc; 9use std::sync::Arc;
11use tokio::io::{AsyncReadExt, AsyncWriteExt}; 10use tokio::io::{AsyncReadExt, AsyncWriteExt};
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 494f8b9..a783782 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -306,11 +306,7 @@ pub fn parse_git_url(path: &str) -> Option<(&str, &str, &str)> {
306 let subpath = parts[2]; 306 let subpath = parts[2];
307 307
308 // Extract identifier (remove .git suffix if present for the middle part) 308 // Extract identifier (remove .git suffix if present for the middle part)
309 let identifier = if repo_part.ends_with(".git") { 309 let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part);
310 &repo_part[..repo_part.len() - 4]
311 } else {
312 repo_part
313 };
314 310
315 Some((npub, identifier, subpath)) 311 Some((npub, identifier, subpath))
316} 312}
@@ -343,7 +339,12 @@ mod tests {
343 339
344 // Initialize bare repository 340 // Initialize bare repository
345 Command::new("git") 341 Command::new("git")
346 .args(["init", "--bare", "--initial-branch=main", bare_repo.to_str().unwrap()]) 342 .args([
343 "init",
344 "--bare",
345 "--initial-branch=main",
346 bare_repo.to_str().unwrap(),
347 ])
347 .output() 348 .output()
348 .unwrap(); 349 .unwrap();
349 350
diff --git a/src/git/protocol.rs b/src/git/protocol.rs
index 93177de..8592c27 100644
--- a/src/git/protocol.rs
+++ b/src/git/protocol.rs
@@ -55,11 +55,11 @@ impl PktLine {
55 return Err(ProtocolError::InsufficientData); 55 return Err(ProtocolError::InsufficientData);
56 } 56 }
57 57
58 let len_str = std::str::from_utf8(&input[0..4]) 58 let len_str =
59 .map_err(|_| ProtocolError::InvalidLength)?; 59 std::str::from_utf8(&input[0..4]).map_err(|_| ProtocolError::InvalidLength)?;
60 60
61 let len = u16::from_str_radix(len_str, 16) 61 let len =
62 .map_err(|_| ProtocolError::InvalidLength)? as usize; 62 u16::from_str_radix(len_str, 16).map_err(|_| ProtocolError::InvalidLength)? as usize;
63 63
64 if len == 0 { 64 if len == 0 {
65 // Flush packet 65 // Flush packet
@@ -81,19 +81,19 @@ impl PktLine {
81 /// Parse all pkt-lines from bytes 81 /// Parse all pkt-lines from bytes
82 pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> { 82 pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> {
83 let mut packets = Vec::new(); 83 let mut packets = Vec::new();
84 84
85 while !input.is_empty() { 85 while !input.is_empty() {
86 let (packet, remaining) = Self::parse(input)?; 86 let (packet, remaining) = Self::parse(input)?;
87 let is_flush = matches!(packet, PktLine::Flush); 87 let is_flush = matches!(packet, PktLine::Flush);
88 packets.push(packet); 88 packets.push(packet);
89 input = remaining; 89 input = remaining;
90 90
91 // Stop at flush packet 91 // Stop at flush packet
92 if is_flush { 92 if is_flush {
93 break; 93 break;
94 } 94 }
95 } 95 }
96 96
97 Ok(packets) 97 Ok(packets)
98 } 98 }
99} 99}
@@ -259,4 +259,4 @@ mod tests {
259 "application/x-git-upload-pack-result" 259 "application/x-git-upload-pack-result"
260 ); 260 );
261 } 261 }
262} \ No newline at end of file 262}
diff --git a/src/git/subprocess.rs b/src/git/subprocess.rs
index c95bce5..2d9a981 100644
--- a/src/git/subprocess.rs
+++ b/src/git/subprocess.rs
@@ -28,9 +28,9 @@ impl GitSubprocess {
28 advertise: bool, 28 advertise: bool,
29 ) -> std::io::Result<Self> { 29 ) -> std::io::Result<Self> {
30 let repo_path = repo_path.as_ref(); 30 let repo_path = repo_path.as_ref();
31 31
32 let mut cmd = Command::new("git"); 32 let mut cmd = Command::new("git");
33 33
34 // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and 34 // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and
35 // `allow-tip-sha1-in-want` in advertisement and serve available oids. 35 // `allow-tip-sha1-in-want` in advertisement and serve available oids.
36 // These config options must be passed before the command name. 36 // These config options must be passed before the command name.
@@ -38,22 +38,22 @@ impl GitSubprocess {
38 cmd.arg("uploadpack.allowReachableSHA1InWant=true"); 38 cmd.arg("uploadpack.allowReachableSHA1InWant=true");
39 cmd.arg("-c"); 39 cmd.arg("-c");
40 cmd.arg("uploadpack.allowTipSHA1InWant=true"); 40 cmd.arg("uploadpack.allowTipSHA1InWant=true");
41 41
42 cmd.arg(service.command_name()); 42 cmd.arg(service.command_name());
43 43
44 if advertise { 44 if advertise {
45 cmd.arg("--advertise-refs"); 45 cmd.arg("--advertise-refs");
46 } 46 }
47 47
48 cmd.arg("--stateless-rpc"); 48 cmd.arg("--stateless-rpc");
49 cmd.arg(repo_path); 49 cmd.arg(repo_path);
50 50
51 cmd.stdin(Stdio::piped()); 51 cmd.stdin(Stdio::piped());
52 cmd.stdout(Stdio::piped()); 52 cmd.stdout(Stdio::piped());
53 cmd.stderr(Stdio::piped()); 53 cmd.stderr(Stdio::piped());
54 54
55 let child = cmd.spawn()?; 55 let child = cmd.spawn()?;
56 56
57 Ok(Self { child }) 57 Ok(Self { child })
58 } 58 }
59 59
@@ -101,8 +101,8 @@ impl GitSubprocess {
101#[cfg(test)] 101#[cfg(test)]
102mod tests { 102mod tests {
103 use super::*; 103 use super::*;
104 use tempfile::TempDir;
105 use std::process::Command as StdCommand; 104 use std::process::Command as StdCommand;
105 use tempfile::TempDir;
106 106
107 fn create_bare_repo() -> TempDir { 107 fn create_bare_repo() -> TempDir {
108 let dir = TempDir::new().unwrap(); 108 let dir = TempDir::new().unwrap();
@@ -118,11 +118,8 @@ mod tests {
118 #[tokio::test] 118 #[tokio::test]
119 async fn test_spawn_upload_pack_advertise() { 119 async fn test_spawn_upload_pack_advertise() {
120 let repo = create_bare_repo(); 120 let repo = create_bare_repo();
121 let mut proc = GitSubprocess::spawn( 121 let mut proc = GitSubprocess::spawn(GitService::UploadPack, repo.path(), true)
122 GitService::UploadPack, 122 .expect("Failed to spawn git");
123 repo.path(),
124 true,
125 ).expect("Failed to spawn git");
126 123
127 // Should have spawned successfully 124 // Should have spawned successfully
128 assert!(proc.stdout().is_some()); 125 assert!(proc.stdout().is_some());
@@ -135,15 +132,12 @@ mod tests {
135 #[tokio::test] 132 #[tokio::test]
136 async fn test_spawn_receive_pack() { 133 async fn test_spawn_receive_pack() {
137 let repo = create_bare_repo(); 134 let repo = create_bare_repo();
138 let mut proc = GitSubprocess::spawn( 135 let mut proc = GitSubprocess::spawn(GitService::ReceivePack, repo.path(), false)
139 GitService::ReceivePack, 136 .expect("Failed to spawn git");
140 repo.path(),
141 false,
142 ).expect("Failed to spawn git");
143 137
144 assert!(proc.stdout().is_some()); 138 assert!(proc.stdout().is_some());
145 assert!(proc.stdin().is_some()); 139 assert!(proc.stdin().is_some());
146 140
147 let _ = proc.kill().await; 141 let _ = proc.kill().await;
148 } 142 }
149} \ No newline at end of file 143}
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 07b47ee..f43cf86 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -9,20 +9,20 @@ use std::net::SocketAddr;
9use std::pin::Pin; 9use std::pin::Pin;
10use std::sync::Arc; 10use std::sync::Arc;
11 11
12use base64::Engine;
13use http_body_util::{BodyExt, Full};
12use hyper::body::{Bytes, Incoming}; 14use hyper::body::{Bytes, Incoming};
13use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; 15use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE};
14use hyper::server::conn::http1; 16use hyper::server::conn::http1;
15use hyper::service::Service; 17use hyper::service::Service;
16use hyper::{Method, Request, Response}; 18use hyper::{Method, Request, Response};
17use hyper_util::rt::TokioIo; 19use hyper_util::rt::TokioIo;
18use http_body_util::{BodyExt, Full}; 20use nostr_relay_builder::prelude::MemoryDatabase;
21use nostr_relay_builder::LocalRelay;
19use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 22use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
20use nostr_sdk::hashes::{Hash, HashEngine}; 23use nostr_sdk::hashes::{Hash, HashEngine};
21use nostr_sdk::PublicKey; 24use nostr_sdk::PublicKey;
22use nostr_relay_builder::prelude::MemoryDatabase;
23use nostr_relay_builder::LocalRelay;
24use tokio::net::TcpListener; 25use tokio::net::TcpListener;
25use base64::Engine;
26 26
27use crate::config::Config; 27use crate::config::Config;
28use crate::git; 28use crate::git;
@@ -50,7 +50,12 @@ struct HttpService {
50} 50}
51 51
52impl HttpService { 52impl HttpService {
53 fn new(relay: LocalRelay, config: Config, remote: SocketAddr, database: Arc<MemoryDatabase>) -> Self { 53 fn new(
54 relay: LocalRelay,
55 config: Config,
56 remote: SocketAddr,
57 database: Arc<MemoryDatabase>,
58 ) -> Self {
54 Self { 59 Self {
55 relay, 60 relay,
56 config, 61 config,
@@ -77,10 +82,12 @@ impl Service<Request<Incoming>> for HttpService {
77 // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content 82 // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content
78 if method == Method::OPTIONS { 83 if method == Method::OPTIONS {
79 return Box::pin(async move { 84 return Box::pin(async move {
80 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) 85 Ok(
81 .status(204) 86 add_cors_headers(Response::builder().header("server", "ngit-grasp"))
82 .body(Full::new(Bytes::new())) 87 .status(204)
83 .unwrap()) 88 .body(Full::new(Bytes::new()))
89 .unwrap(),
90 )
84 }); 91 });
85 } 92 }
86 93
@@ -89,41 +96,47 @@ impl Service<Request<Incoming>> for HttpService {
89 let npub = npub.to_string(); 96 let npub = npub.to_string();
90 let identifier = identifier.to_string(); 97 let identifier = identifier.to_string();
91 let subpath = subpath.to_string(); 98 let subpath = subpath.to_string();
92 99
93 tracing::debug!("Git request: {} {} (npub={}, id={}, subpath={})", 100 tracing::debug!(
94 method, path, npub, identifier, subpath); 101 "Git request: {} {} (npub={}, id={}, subpath={})",
102 method,
103 path,
104 npub,
105 identifier,
106 subpath
107 );
95 108
96 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); 109 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier);
97 110
98 return Box::pin(async move { 111 return Box::pin(async move {
99 // Collect request body once before the match statement 112 // Collect request body once before the match statement
100 let body_bytes = req.collect().await 113 let body_bytes = req
114 .collect()
115 .await
101 .map(|collected| collected.to_bytes()) 116 .map(|collected| collected.to_bytes())
102 .unwrap_or_else(|_| Bytes::new()); 117 .unwrap_or_else(|_| Bytes::new());
103 118
104 let result = match (method.as_ref(), subpath.as_str()) { 119 let result = match (method.as_ref(), subpath.as_str()) {
105 // GET /info/refs?service=git-upload-pack or git-receive-pack 120 // GET /info/refs?service=git-upload-pack or git-receive-pack
106 (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { 121 (m, sp) if m == Method::GET && sp.starts_with("info/refs") => {
107 // Parse query string for service parameter 122 // Parse query string for service parameter
108 let service = query.as_deref().unwrap_or("") 123 let service = query
124 .as_deref()
125 .unwrap_or("")
109 .strip_prefix("service=") 126 .strip_prefix("service=")
110 .and_then(git::protocol::GitService::from_query_param); 127 .and_then(git::protocol::GitService::from_query_param);
111 128
112 match service { 129 match service {
113 Some(svc) => { 130 Some(svc) => git::handlers::handle_info_refs(repo_path, svc).await,
114 git::handlers::handle_info_refs(repo_path, svc).await 131 None => Err(git::handlers::GitError::RepositoryNotFound),
115 }
116 None => {
117 Err(git::handlers::GitError::RepositoryNotFound)
118 }
119 } 132 }
120 } 133 }
121 134
122 // POST /git-upload-pack (clone/fetch) 135 // POST /git-upload-pack (clone/fetch)
123 (m, "git-upload-pack") if m == Method::POST => { 136 (m, "git-upload-pack") if m == Method::POST => {
124 git::handlers::handle_upload_pack(repo_path, body_bytes).await 137 git::handlers::handle_upload_pack(repo_path, body_bytes).await
125 } 138 }
126 139
127 // POST /git-receive-pack (push) - with GRASP authorization via database 140 // POST /git-receive-pack (push) - with GRASP authorization via database
128 (m, "git-receive-pack") if m == Method::POST => { 141 (m, "git-receive-pack") if m == Method::POST => {
129 // Convert npub (bech32) to hex pubkey for authorization 142 // Convert npub (bech32) to hex pubkey for authorization
@@ -137,33 +150,41 @@ impl Service<Request<Incoming>> for HttpService {
137 .unwrap()); 150 .unwrap());
138 } 151 }
139 }; 152 };
140 153
141 git::handlers::handle_receive_pack( 154 git::handlers::handle_receive_pack(
142 repo_path, 155 repo_path,
143 body_bytes.clone(), 156 body_bytes.clone(),
144 Some(database.clone()), 157 Some(database.clone()),
145 &identifier, 158 &identifier,
146 &owner_pubkey_hex, 159 &owner_pubkey_hex,
147 ).await 160 )
148 } 161 .await
149
150 _ => {
151 Err(git::handlers::GitError::RepositoryNotFound)
152 } 162 }
163
164 _ => Err(git::handlers::GitError::RepositoryNotFound),
153 }; 165 };
154 166
155 match result { 167 match result {
156 Ok(response) => { 168 Ok(response) => {
157 // Add CORS headers to successful Git responses 169 // Add CORS headers to successful Git responses
158 let (parts, body) = response.into_parts(); 170 let (parts, body) = response.into_parts();
159 Ok(add_cors_headers(Response::builder() 171 Ok(add_cors_headers(Response::builder().status(parts.status))
160 .status(parts.status)) 172 .header(
161 .header("content-type", parts.headers.get("content-type") 173 "content-type",
162 .and_then(|v| v.to_str().ok()) 174 parts
163 .unwrap_or("application/octet-stream")) 175 .headers
164 .header("cache-control", parts.headers.get("cache-control") 176 .get("content-type")
165 .and_then(|v| v.to_str().ok()) 177 .and_then(|v| v.to_str().ok())
166 .unwrap_or("no-cache")) 178 .unwrap_or("application/octet-stream"),
179 )
180 .header(
181 "cache-control",
182 parts
183 .headers
184 .get("cache-control")
185 .and_then(|v| v.to_str().ok())
186 .unwrap_or("no-cache"),
187 )
167 .body(body) 188 .body(body)
168 .unwrap()) 189 .unwrap())
169 } 190 }
@@ -191,15 +212,20 @@ impl Service<Request<Incoming>> for HttpService {
191 tracing::error!("Failed to serialize NIP-11 document: {}", e); 212 tracing::error!("Failed to serialize NIP-11 document: {}", e);
192 "{}".to_string() 213 "{}".to_string()
193 }); 214 });
194 215
195 tracing::debug!("Serving NIP-11 relay information document to {}", self.remote); 216 tracing::debug!(
196 217 "Serving NIP-11 relay information document to {}",
218 self.remote
219 );
220
197 return Box::pin(async move { 221 return Box::pin(async move {
198 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) 222 Ok(
199 .status(200) 223 add_cors_headers(Response::builder().header("server", "ngit-grasp"))
200 .header("content-type", "application/nostr+json") 224 .status(200)
201 .body(Full::new(Bytes::from(json))) 225 .header("content-type", "application/nostr+json")
202 .unwrap()) 226 .body(Full::new(Bytes::from(json)))
227 .unwrap(),
228 )
203 }); 229 });
204 } 230 }
205 } 231 }
@@ -221,12 +247,13 @@ impl Service<Request<Incoming>> for HttpService {
221 247
222 let addr = self.remote; 248 let addr = self.remote;
223 let relay = self.relay.clone(); 249 let relay = self.relay.clone();
224 250
225 tokio::spawn(async move { 251 tokio::spawn(async move {
226 match hyper::upgrade::on(req).await { 252 match hyper::upgrade::on(req).await {
227 Ok(upgraded) => { 253 Ok(upgraded) => {
228 tracing::info!("WebSocket connection established from {}", addr); 254 tracing::info!("WebSocket connection established from {}", addr);
229 if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await 255 if let Err(e) =
256 relay.take_connection(TokioIo::new(upgraded), addr).await
230 { 257 {
231 tracing::error!("Relay error for {}: {}", addr, e); 258 tracing::error!("Relay error for {}: {}", addr, e);
232 } 259 }
@@ -288,12 +315,12 @@ pub async fn run_server(
288 tracing::info!("Domain: {}", config.domain); 315 tracing::info!("Domain: {}", config.domain);
289 316
290 let listener = TcpListener::bind(&bind_addr).await?; 317 let listener = TcpListener::bind(&bind_addr).await?;
291 318
292 loop { 319 loop {
293 let (socket, addr) = listener.accept().await?; 320 let (socket, addr) = listener.accept().await?;
294 let io = TokioIo::new(socket); 321 let io = TokioIo::new(socket);
295 let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone()); 322 let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone());
296 323
297 tokio::spawn(async move { 324 tokio::spawn(async move {
298 if let Err(e) = http1::Builder::new() 325 if let Err(e) = http1::Builder::new()
299 .serve_connection(io, service) 326 .serve_connection(io, service)
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
index a93ee5f..593ef9a 100644
--- a/src/http/nip11.rs
+++ b/src/http/nip11.rs
@@ -1,10 +1,9 @@
1use crate::config::Config;
1/// NIP-11 Relay Information Document 2/// NIP-11 Relay Information Document
2/// 3///
3/// Implements NIP-11 relay information endpoint with GRASP-01 extensions. 4/// Implements NIP-11 relay information endpoint with GRASP-01 extensions.
4/// See: https://github.com/nostr-protocol/nips/blob/master/11.md 5/// See: https://github.com/nostr-protocol/nips/blob/master/11.md
5
6use serde::{Deserialize, Serialize}; 6use serde::{Deserialize, Serialize};
7use crate::config::Config;
8 7
9/// NIP-11 Relay Information Document 8/// NIP-11 Relay Information Document
10/// 9///
@@ -14,37 +13,36 @@ use crate::config::Config;
14pub struct RelayInformationDocument { 13pub struct RelayInformationDocument {
15 /// Relay name 14 /// Relay name
16 pub name: String, 15 pub name: String,
17 16
18 /// Relay description 17 /// Relay description
19 pub description: String, 18 pub description: String,
20 19
21 /// Relay owner's public key (hex format) 20 /// Relay owner's public key (hex format)
22 #[serde(skip_serializing_if = "Option::is_none")] 21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub pubkey: Option<String>, 22 pub pubkey: Option<String>,
24 23
25 /// Contact information for relay admin 24 /// Contact information for relay admin
26 #[serde(skip_serializing_if = "Option::is_none")] 25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub contact: Option<String>, 26 pub contact: Option<String>,
28 27
29 /// List of NIPs supported by this relay 28 /// List of NIPs supported by this relay
30 pub supported_nips: Vec<u16>, 29 pub supported_nips: Vec<u16>,
31 30
32 /// Relay software identifier 31 /// Relay software identifier
33 pub software: String, 32 pub software: String,
34 33
35 /// Software version 34 /// Software version
36 pub version: String, 35 pub version: String,
37 36
38 // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec) 37 // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec)
39
40 /// List of supported GRASPs (e.g., ["GRASP-01"]) 38 /// List of supported GRASPs (e.g., ["GRASP-01"])
41 /// Required by GRASP-01 specification line 12 39 /// Required by GRASP-01 specification line 12
42 pub supported_grasps: Vec<String>, 40 pub supported_grasps: Vec<String>,
43 41
44 /// Repository acceptance criteria description 42 /// Repository acceptance criteria description
45 /// Required by GRASP-01 specification line 13 43 /// Required by GRASP-01 specification line 13
46 pub repo_acceptance_criteria: String, 44 pub repo_acceptance_criteria: String,
47 45
48 /// Curation policy (present if curated, absent otherwise) 46 /// Curation policy (present if curated, absent otherwise)
49 /// Optional per GRASP-01 specification line 14 47 /// Optional per GRASP-01 specification line 14
50 #[serde(skip_serializing_if = "Option::is_none")] 48 #[serde(skip_serializing_if = "Option::is_none")]
@@ -66,7 +64,7 @@ impl RelayInformationDocument {
66 ], 64 ],
67 software: env!("CARGO_PKG_NAME").to_string(), 65 software: env!("CARGO_PKG_NAME").to_string(),
68 version: env!("CARGO_PKG_VERSION").to_string(), 66 version: env!("CARGO_PKG_VERSION").to_string(),
69 67
70 // GRASP-01 Extensions 68 // GRASP-01 Extensions
71 supported_grasps: vec!["GRASP-01".to_string()], 69 supported_grasps: vec!["GRASP-01".to_string()],
72 repo_acceptance_criteria: format!( 70 repo_acceptance_criteria: format!(
@@ -77,7 +75,7 @@ impl RelayInformationDocument {
77 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy 75 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy
78 } 76 }
79 } 77 }
80 78
81 /// Serialize to JSON string 79 /// Serialize to JSON string
82 pub fn to_json(&self) -> Result<String, serde_json::Error> { 80 pub fn to_json(&self) -> Result<String, serde_json::Error> {
83 serde_json::to_string_pretty(self) 81 serde_json::to_string_pretty(self)
@@ -102,7 +100,7 @@ mod tests {
102 }; 100 };
103 101
104 let doc = RelayInformationDocument::from_config(&config); 102 let doc = RelayInformationDocument::from_config(&config);
105 103
106 assert_eq!(doc.name, "Test Relay"); 104 assert_eq!(doc.name, "Test Relay");
107 assert_eq!(doc.description, "A test relay"); 105 assert_eq!(doc.description, "A test relay");
108 assert_eq!(doc.pubkey, Some("npub1test".to_string())); 106 assert_eq!(doc.pubkey, Some("npub1test".to_string()));
@@ -129,7 +127,7 @@ mod tests {
129 127
130 let doc = RelayInformationDocument::from_config(&config); 128 let doc = RelayInformationDocument::from_config(&config);
131 let json = doc.to_json().expect("Failed to serialize to JSON"); 129 let json = doc.to_json().expect("Failed to serialize to JSON");
132 130
133 // Verify JSON contains expected fields 131 // Verify JSON contains expected fields
134 assert!(json.contains("\"name\"")); 132 assert!(json.contains("\"name\""));
135 assert!(json.contains("\"description\"")); 133 assert!(json.contains("\"description\""));
@@ -137,10 +135,10 @@ mod tests {
137 assert!(json.contains("\"supported_grasps\"")); 135 assert!(json.contains("\"supported_grasps\""));
138 assert!(json.contains("\"repo_acceptance_criteria\"")); 136 assert!(json.contains("\"repo_acceptance_criteria\""));
139 assert!(json.contains("GRASP-01")); 137 assert!(json.contains("GRASP-01"));
140 138
141 // Verify it's valid JSON by parsing 139 // Verify it's valid JSON by parsing
142 let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); 140 let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON");
143 assert_eq!(parsed["name"], "Test Relay"); 141 assert_eq!(parsed["name"], "Test Relay");
144 assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); 142 assert_eq!(parsed["supported_grasps"][0], "GRASP-01");
145 } 143 }
146} \ No newline at end of file 144}
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 8e9926a..97fd17e 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -51,7 +51,7 @@ impl Nip34WritePolicy {
51 /// Create a bare git repository if it doesn't exist 51 /// Create a bare git repository if it doesn't exist
52 /// Path format: <git_data_path>/<npub>/<identifier>.git 52 /// Path format: <git_data_path>/<npub>/<identifier>.git
53 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { 53 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> {
54 let repo_path = self.git_data_path.join(&announcement.repo_path()); 54 let repo_path = self.git_data_path.join(announcement.repo_path());
55 55
56 // Check if repository already exists 56 // Check if repository already exists
57 if repo_path.exists() { 57 if repo_path.exists() {
@@ -69,7 +69,7 @@ impl Nip34WritePolicy {
69 69
70 // Initialize bare repository using git command 70 // Initialize bare repository using git command
71 let output = std::process::Command::new("git") 71 let output = std::process::Command::new("git")
72 .args(&["init", "--bare", repo_path.to_str().unwrap()]) 72 .args(["init", "--bare", repo_path.to_str().unwrap()])
73 .output() 73 .output()
74 .map_err(|e| format!("Failed to execute git init: {}", e))?; 74 .map_err(|e| format!("Failed to execute git init: {}", e))?;
75 75
@@ -482,7 +482,7 @@ impl Nip34WritePolicy {
482 }; 482 };
483 483
484 // Build repository path 484 // Build repository path
485 let repo_path = self.git_data_path.join(&announcement.repo_path()); 485 let repo_path = self.git_data_path.join(announcement.repo_path());
486 486
487 // Validate the ref 487 // Validate the ref
488 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) { 488 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) {
@@ -631,8 +631,8 @@ impl Nip34WritePolicy {
631 let kind_u16 = event.kind.as_u16(); 631 let kind_u16 = event.kind.as_u16();
632 632
633 // Check if this is any kind of replaceable event 633 // Check if this is any kind of replaceable event
634 let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; 634 let is_regular_replaceable = (10000..20000).contains(&kind_u16);
635 let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; 635 let is_parameterized_replaceable = (30000..40000).contains(&kind_u16);
636 636
637 if is_regular_replaceable || is_parameterized_replaceable { 637 if is_regular_replaceable || is_parameterized_replaceable {
638 // Build the appropriate address format based on event type 638 // Build the appropriate address format based on event type
@@ -669,7 +669,7 @@ impl Nip34WritePolicy {
669 ]; 669 ];
670 670
671 for tag_type in &addressable_tags { 671 for tag_type in &addressable_tags {
672 let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); 672 let filter = Filter::new().custom_tag(*tag_type, address.clone());
673 673
674 match database.query(filter).await { 674 match database.query(filter).await {
675 Ok(events) => { 675 Ok(events) => {
@@ -691,7 +691,7 @@ impl Nip34WritePolicy {
691 ]; 691 ];
692 692
693 for tag_type in &event_id_tags { 693 for tag_type in &event_id_tags {
694 let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); 694 let filter = Filter::new().custom_tag(*tag_type, event_id_hex.clone());
695 695
696 match database.query(filter).await { 696 match database.query(filter).await {
697 Ok(events) => { 697 Ok(events) => {
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 6a62ccd..050bfdd 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -322,9 +322,9 @@ impl RepositoryState {
322 322
323 /// Get the HEAD branch name (without refs/heads/ prefix) 323 /// Get the HEAD branch name (without refs/heads/ prefix)
324 pub fn get_head_branch(&self) -> Option<&str> { 324 pub fn get_head_branch(&self) -> Option<&str> {
325 self.head.as_ref().and_then(|h| { 325 self.head
326 h.strip_prefix("refs/heads/") 326 .as_ref()
327 }) 327 .and_then(|h| h.strip_prefix("refs/heads/"))
328 } 328 }
329 329
330 /// Check if the HEAD commit is available in the git repository 330 /// Check if the HEAD commit is available in the git repository
@@ -397,7 +397,7 @@ pub fn validate_state(event: &Event) -> Result<()> {
397#[cfg(test)] 397#[cfg(test)]
398mod tests { 398mod tests {
399 use super::*; 399 use super::*;
400 use nostr_sdk::{EventBuilder, Keys, Tag}; 400 use nostr_sdk::{EventBuilder, Keys};
401 401
402 fn create_test_keys() -> Keys { 402 fn create_test_keys() -> Keys {
403 Keys::generate() 403 Keys::generate()
@@ -618,7 +618,10 @@ mod tests {
618 618
619 let announcement = RepositoryAnnouncement::from_event(event).unwrap(); 619 let announcement = RepositoryAnnouncement::from_event(event).unwrap();
620 assert_eq!(announcement.maintainers.len(), 1); 620 assert_eq!(announcement.maintainers.len(), 1);
621 assert_eq!(announcement.maintainers[0], maintainer_keys.public_key().to_hex()); 621 assert_eq!(
622 announcement.maintainers[0],
623 maintainer_keys.public_key().to_hex()
624 );
622 } 625 }
623 626
624 #[test] 627 #[test]
@@ -727,10 +730,7 @@ mod tests {
727 730
728 let keys = create_test_keys(); 731 let keys = create_test_keys();
729 let tags = vec![ 732 let tags = vec![
730 Tag::custom( 733 Tag::custom(nostr_sdk::TagKind::d(), vec!["test-repo".to_string()]),
731 nostr_sdk::TagKind::d(),
732 vec!["test-repo".to_string()],
733 ),
734 Tag::custom( 734 Tag::custom(
735 nostr_sdk::TagKind::Custom("refs/heads/main".into()), 735 nostr_sdk::TagKind::Custom("refs/heads/main".into()),
736 vec!["a1b2c3d4".to_string()], 736 vec!["a1b2c3d4".to_string()],