commit 3f659008241fb347dad36de3e98b90b6ff56caf6
parent e211b80133a563ab5838a5a0675b612a7c92ff41
Author: triesap <tyson@radroots.org>
Date: Thu, 11 Jun 2026 16:57:12 -0700
events: add farm file and auth models
- add NIP-94-compatible Field file metadata models separate from message files
- add NIP-42 relay auth and NIP-98 HTTP auth event models
- cover file and auth serde shapes with unit tests
Diffstat:
4 files changed, 221 insertions(+), 0 deletions(-)
diff --git a/crates/events/src/farm_file.rs b/crates/events/src/farm_file.rs
@@ -0,0 +1,123 @@
+#![forbid(unsafe_code)]
+
+use crate::farm_crdt::RadrootsFarmCrdtDocumentKind;
+use crate::farm_workspace::RadrootsFarmWorkspaceRef;
+use crate::kinds::KIND_FARM_FILE_METADATA as KIND_FARM_FILE_METADATA_EVENT;
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+pub const KIND_FARM_FILE_METADATA: u32 = KIND_FARM_FILE_METADATA_EVENT;
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsFarmFileMetadata {
+ pub d_tag: String,
+ pub workspace: RadrootsFarmWorkspaceRef,
+ pub farm_group_id: String,
+ pub owner_document_id: String,
+ pub owner_document_kind: RadrootsFarmCrdtDocumentKind,
+ pub caption: Option<String>,
+ pub url: String,
+ pub mime_type: String,
+ pub sha256: String,
+ pub original_sha256: Option<String>,
+ pub size_bytes: Option<u64>,
+ pub dimensions: Option<RadrootsFarmFileDimensions>,
+ pub blurhash: Option<String>,
+ pub thumb: Option<RadrootsFarmFileSource>,
+ pub image: Option<RadrootsFarmFileSource>,
+ pub alt: Option<String>,
+ pub fallbacks: Vec<String>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct RadrootsFarmFileDimensions {
+ pub w: u32,
+ pub h: u32,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsFarmFileSource {
+ pub url: String,
+ pub mime_type: Option<String>,
+ pub dimensions: Option<RadrootsFarmFileDimensions>,
+}
+
+#[cfg(all(test, feature = "serde"))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn file_metadata_kind_uses_nip94_file_metadata_kind() {
+ assert_eq!(KIND_FARM_FILE_METADATA, 1063);
+ }
+
+ #[test]
+ fn file_metadata_remains_separate_from_message_file_model() {
+ let metadata = sample_file_metadata();
+
+ assert_eq!(metadata.d_tag, "EFGHIJKLMNOPQRSTUVWXYZ");
+ assert_eq!(metadata.owner_document_id, "DEFGHIJKLMNOPQRSTUVWXY");
+ assert_eq!(
+ metadata.owner_document_kind,
+ RadrootsFarmCrdtDocumentKind::FarmTask
+ );
+ assert_eq!(metadata.caption.as_deref(), Some("Tomatoes harvested from Patch Y."));
+ assert_eq!(metadata.mime_type, "image/jpeg");
+ assert_eq!(metadata.dimensions, Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }));
+ assert_eq!(metadata.fallbacks.len(), 1);
+ }
+
+ #[test]
+ fn file_metadata_serializes_stable_content_shape() {
+ let value = serde_json::to_value(sample_file_metadata()).unwrap();
+
+ assert_eq!(value["workspace"]["d_tag"], "ABCDEFGHIJKLMNOPQRSTUV");
+ assert_eq!(value["farm_group_id"], "BCDEFGHIJKLMNOPQRSTUVW");
+ assert_eq!(value["owner_document_kind"], "FarmTask");
+ assert_eq!(value["caption"], "Tomatoes harvested from Patch Y.");
+ assert_eq!(value["mime_type"], "image/jpeg");
+ assert_eq!(
+ value["sha256"],
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ );
+ assert_eq!(value["dimensions"]["w"], 1600);
+ assert_eq!(value["dimensions"]["h"], 1200);
+ }
+
+ fn sample_file_metadata() -> RadrootsFarmFileMetadata {
+ RadrootsFarmFileMetadata {
+ d_tag: "EFGHIJKLMNOPQRSTUVWXYZ".to_string(),
+ workspace: RadrootsFarmWorkspaceRef {
+ pubkey: "workspace_pubkey".to_string(),
+ d_tag: "ABCDEFGHIJKLMNOPQRSTUV".to_string(),
+ },
+ farm_group_id: "BCDEFGHIJKLMNOPQRSTUVW".to_string(),
+ owner_document_id: "DEFGHIJKLMNOPQRSTUVWXY".to_string(),
+ owner_document_kind: RadrootsFarmCrdtDocumentKind::FarmTask,
+ caption: Some("Tomatoes harvested from Patch Y.".to_string()),
+ url: "https://media.example.invalid/blob/sha256".to_string(),
+ mime_type: "image/jpeg".to_string(),
+ sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ .to_string(),
+ original_sha256: Some(
+ "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
+ .to_string(),
+ ),
+ size_bytes: Some(123_456),
+ dimensions: Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }),
+ blurhash: Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string()),
+ thumb: Some(RadrootsFarmFileSource {
+ url: "https://media.example.invalid/thumb/sha256".to_string(),
+ mime_type: Some("image/jpeg".to_string()),
+ dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 240 }),
+ }),
+ image: None,
+ alt: Some("Harvested tomatoes in a crate".to_string()),
+ fallbacks: vec!["https://fallback.example.invalid/blob/sha256".to_string()],
+ }
+ }
+}
diff --git a/crates/events/src/http_auth.rs b/crates/events/src/http_auth.rs
@@ -0,0 +1,56 @@
+#![forbid(unsafe_code)]
+
+use crate::kinds::KIND_HTTP_AUTH as KIND_HTTP_AUTH_EVENT;
+
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+pub const KIND_HTTP_AUTH: u32 = KIND_HTTP_AUTH_EVENT;
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsHttpAuth {
+ pub url: String,
+ pub method: String,
+ pub payload_sha256: Option<String>,
+}
+
+#[cfg(all(test, feature = "serde"))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn http_auth_kind_matches_nip98() {
+ assert_eq!(KIND_HTTP_AUTH, 27235);
+ }
+
+ #[test]
+ fn http_auth_serializes_optional_payload_hash() {
+ let value = serde_json::to_value(RadrootsHttpAuth {
+ url: "https://media.example.invalid/upload".to_string(),
+ method: "POST".to_string(),
+ payload_sha256: Some(
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
+ ),
+ })
+ .unwrap();
+
+ assert_eq!(value["url"], "https://media.example.invalid/upload");
+ assert_eq!(value["method"], "POST");
+ assert_eq!(
+ value["payload_sha256"],
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ );
+ }
+
+ #[test]
+ fn http_auth_allows_absent_payload_hash() {
+ let auth = RadrootsHttpAuth {
+ url: "https://media.example.invalid/download".to_string(),
+ method: "GET".to_string(),
+ payload_sha256: None,
+ };
+
+ assert_eq!(auth.payload_sha256, None);
+ }
+}
diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs
@@ -17,6 +17,7 @@ pub mod farm_workspace;
pub mod follow;
pub mod geochat;
pub mod gift_wrap;
+pub mod http_auth;
pub mod job;
pub mod job_feedback;
pub mod job_request;
@@ -31,6 +32,7 @@ pub mod plot;
pub mod post;
pub mod profile;
pub mod reaction;
+pub mod relay_auth;
pub mod relay_document;
pub mod resource_area;
pub mod resource_cap;
diff --git a/crates/events/src/relay_auth.rs b/crates/events/src/relay_auth.rs
@@ -0,0 +1,40 @@
+#![forbid(unsafe_code)]
+
+use crate::kinds::KIND_RELAY_AUTH as KIND_RELAY_AUTH_EVENT;
+
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+pub const KIND_RELAY_AUTH: u32 = KIND_RELAY_AUTH_EVENT;
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsRelayAuth {
+ pub relay: String,
+ pub challenge: String,
+}
+
+#[cfg(all(test, feature = "serde"))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn relay_auth_kind_matches_nip42() {
+ assert_eq!(KIND_RELAY_AUTH, 22242);
+ }
+
+ #[test]
+ fn relay_auth_serializes_nip42_tags() {
+ let value = serde_json::to_value(RadrootsRelayAuth {
+ relay: "wss://relay.example.invalid/farm/ABCDEFGHIJKLMNOPQRSTUV".to_string(),
+ challenge: "relay-provided-challenge".to_string(),
+ })
+ .unwrap();
+
+ assert_eq!(
+ value["relay"],
+ "wss://relay.example.invalid/farm/ABCDEFGHIJKLMNOPQRSTUV"
+ );
+ assert_eq!(value["challenge"], "relay-provided-challenge");
+ }
+}