listing.rs (17846B)
1 use thiserror::Error; 2 3 use radroots_core::{ 4 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, 5 RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, 6 }; 7 use radroots_events::{ 8 kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}, 9 listing::{ 10 RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, 11 RadrootsListingDeliveryMethod, RadrootsListingEventIndex, RadrootsListingEventMetadata, 12 RadrootsListingFarmRef, RadrootsListingImage, RadrootsListingImageSize, 13 RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, 14 }, 15 plot::RadrootsPlotRef, 16 resource_area::RadrootsResourceAreaRef, 17 RadrootsNostrEvent, 18 }; 19 20 use crate::relay::event::RelayIndexerEvent; 21 22 #[derive(Debug, Error)] 23 pub enum RadrootsListingEventIndexError { 24 #[error("Failed to parse listing from tags")] 25 ParseError, 26 } 27 28 #[derive(Default)] 29 struct ListingBinDraft { 30 quantity: Option<RadrootsCoreQuantity>, 31 price_per_canonical_unit: Option<RadrootsCoreQuantityPrice>, 32 display_amount: Option<RadrootsCoreDecimal>, 33 display_unit: Option<RadrootsCoreUnit>, 34 display_label: Option<String>, 35 display_price: Option<RadrootsCoreMoney>, 36 display_price_unit: Option<RadrootsCoreUnit>, 37 } 38 39 fn parse_listing_from_tags( 40 tags: &[Vec<String>], 41 ) -> Result<RadrootsListing, RadrootsListingEventIndexError> { 42 let get_first = |key: &str| -> Option<String> { 43 tags.iter() 44 .find(|t| { 45 t.get(0) 46 .map(|s| s.eq_ignore_ascii_case(key)) 47 .unwrap_or(false) 48 }) 49 .and_then(|t| t.get(1).cloned()) 50 }; 51 52 let required = |v: Option<String>| v.ok_or(RadrootsListingEventIndexError::ParseError); 53 54 let d_tag = required(get_first("d"))?; 55 let farm_pubkey = required(get_first("p"))?; 56 let farm_pubkey = farm_pubkey.trim().to_string(); 57 if farm_pubkey.is_empty() { 58 return Err(RadrootsListingEventIndexError::ParseError); 59 } 60 let parse_addr = |value: &str| -> Result<(u32, String, String), RadrootsListingEventIndexError> { 61 let mut parts = value.splitn(3, ':'); 62 let kind = parts 63 .next() 64 .and_then(|v| v.parse::<u32>().ok()) 65 .ok_or(RadrootsListingEventIndexError::ParseError)?; 66 let pubkey = parts 67 .next() 68 .ok_or(RadrootsListingEventIndexError::ParseError)? 69 .to_string(); 70 let d_tag = parts 71 .next() 72 .ok_or(RadrootsListingEventIndexError::ParseError)? 73 .to_string(); 74 if pubkey.trim().is_empty() || d_tag.trim().is_empty() { 75 return Err(RadrootsListingEventIndexError::ParseError); 76 } 77 Ok((kind, pubkey, d_tag)) 78 }; 79 80 let mut farm_addr_pubkey: Option<String> = None; 81 let mut farm_d_tag: Option<String> = None; 82 for tag in tags.iter().filter(|t| t.first().map(|k| k == "a").unwrap_or(false)) { 83 let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; 84 let (kind, pubkey, d_tag) = parse_addr(value)?; 85 if kind == KIND_FARM { 86 farm_addr_pubkey = Some(pubkey); 87 farm_d_tag = Some(d_tag); 88 break; 89 } 90 } 91 let farm_addr_pubkey = farm_addr_pubkey.ok_or(RadrootsListingEventIndexError::ParseError)?; 92 let farm_d_tag = farm_d_tag.ok_or(RadrootsListingEventIndexError::ParseError)?; 93 if farm_addr_pubkey != farm_pubkey || farm_d_tag.trim().is_empty() { 94 return Err(RadrootsListingEventIndexError::ParseError); 95 } 96 let farm = RadrootsListingFarmRef { 97 pubkey: farm_pubkey, 98 d_tag: farm_d_tag, 99 }; 100 101 let resource_area = if let Some(tag) = tags 102 .iter() 103 .find(|t| t.first().map(|k| k == "radroots:resource_area").unwrap_or(false)) 104 { 105 let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; 106 let (kind, pubkey, d_tag) = parse_addr(value)?; 107 if kind != KIND_RESOURCE_AREA { 108 return Err(RadrootsListingEventIndexError::ParseError); 109 } 110 Some(RadrootsResourceAreaRef { pubkey, d_tag }) 111 } else { 112 None 113 }; 114 115 let plot = if let Some(tag) = tags 116 .iter() 117 .find(|t| t.first().map(|k| k == "radroots:plot").unwrap_or(false)) 118 { 119 let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; 120 let (kind, pubkey, d_tag) = parse_addr(value)?; 121 if kind != KIND_PLOT { 122 return Err(RadrootsListingEventIndexError::ParseError); 123 } 124 Some(RadrootsPlotRef { pubkey, d_tag }) 125 } else { 126 None 127 }; 128 129 let location_tags: Vec<&Vec<String>> = tags 130 .iter() 131 .filter(|t| t.first().map(|k| k == "location").unwrap_or(false)) 132 .collect(); 133 let product_location = if location_tags.len() > 1 { 134 location_tags.first().and_then(|t| t.get(1).cloned()) 135 } else { 136 None 137 }; 138 139 let product = RadrootsListingProduct { 140 key: required(get_first("key"))?, 141 title: required(get_first("title"))?, 142 category: required(get_first("category"))?, 143 summary: get_first("summary"), 144 process: get_first("process"), 145 lot: get_first("lot"), 146 location: product_location, 147 profile: get_first("profile"), 148 year: get_first("year"), 149 }; 150 151 let parse_decimal = |value: &str| value.parse::<RadrootsCoreDecimal>().ok(); 152 let parse_unit = |value: &str| value.parse::<RadrootsCoreUnit>().ok(); 153 let parse_currency = |value: &str| value.parse::<RadrootsCoreCurrency>().ok(); 154 155 let mut bin_order: Vec<String> = Vec::new(); 156 let mut bin_drafts: std::collections::BTreeMap<String, ListingBinDraft> = 157 std::collections::BTreeMap::new(); 158 159 let mut upsert_bin = |bin_id: String, update: ListingBinDraft| { 160 let entry = bin_drafts.entry(bin_id.clone()).or_default(); 161 if !bin_order.iter().any(|id| id == &bin_id) { 162 bin_order.push(bin_id); 163 } 164 if update.quantity.is_some() { 165 entry.quantity = update.quantity; 166 } 167 if update.price_per_canonical_unit.is_some() { 168 entry.price_per_canonical_unit = update.price_per_canonical_unit; 169 } 170 if update.display_amount.is_some() { 171 entry.display_amount = update.display_amount; 172 } 173 if update.display_unit.is_some() { 174 entry.display_unit = update.display_unit; 175 } 176 if update.display_label.is_some() { 177 entry.display_label = update.display_label; 178 } 179 if update.display_price.is_some() { 180 entry.display_price = update.display_price; 181 } 182 if update.display_price_unit.is_some() { 183 entry.display_price_unit = update.display_price_unit; 184 } 185 }; 186 187 for t in tags 188 .iter() 189 .filter(|t| t.first().map(|k| k == "radroots:bin").unwrap_or(false)) 190 { 191 if t.len() < 4 { 192 continue; 193 } 194 let bin_id = t.get(1).map(|v| v.trim().to_string()).unwrap_or_default(); 195 if bin_id.is_empty() { 196 continue; 197 } 198 let amount = t.get(2).and_then(|v| parse_decimal(v)); 199 let unit = t.get(3).and_then(|v| parse_unit(v)); 200 let (Some(amount), Some(unit)) = (amount, unit) else { 201 continue; 202 }; 203 let mut draft = ListingBinDraft::default(); 204 draft.quantity = Some(RadrootsCoreQuantity { 205 amount, 206 unit, 207 label: None, 208 }); 209 let display_amount = t.get(4).and_then(|v| parse_decimal(v)); 210 let display_unit = t.get(5).and_then(|v| parse_unit(v)); 211 if let (Some(display_amount), Some(display_unit)) = (display_amount, display_unit) { 212 draft.display_amount = Some(display_amount); 213 draft.display_unit = Some(display_unit); 214 let label = t 215 .get(6) 216 .map(|v| v.trim().to_string()) 217 .filter(|v| !v.is_empty()); 218 draft.display_label = label; 219 } 220 upsert_bin(bin_id, draft); 221 } 222 223 for t in tags 224 .iter() 225 .filter(|t| t.first().map(|k| k == "radroots:price").unwrap_or(false)) 226 { 227 if t.len() < 6 { 228 continue; 229 } 230 let bin_id = t.get(1).map(|v| v.trim().to_string()).unwrap_or_default(); 231 if bin_id.is_empty() { 232 continue; 233 } 234 let money_amount = t.get(2).and_then(|v| parse_decimal(v)); 235 let money_currency = t.get(3).and_then(|v| parse_currency(v)); 236 let qty_amount = t.get(4).and_then(|v| parse_decimal(v)); 237 let qty_unit = t.get(5).and_then(|v| parse_unit(v)); 238 let (Some(money_amount), Some(money_currency), Some(qty_amount), Some(qty_unit)) = 239 (money_amount, money_currency, qty_amount, qty_unit) 240 else { 241 continue; 242 }; 243 let mut draft = ListingBinDraft::default(); 244 draft.price_per_canonical_unit = Some(RadrootsCoreQuantityPrice { 245 amount: RadrootsCoreMoney { 246 amount: money_amount, 247 currency: money_currency, 248 }, 249 quantity: RadrootsCoreQuantity { 250 amount: qty_amount, 251 unit: qty_unit, 252 label: None, 253 }, 254 }); 255 let display_amount = t.get(6).and_then(|v| parse_decimal(v)); 256 let display_unit = t.get(7).and_then(|v| parse_unit(v)); 257 if let (Some(display_amount), Some(display_unit)) = (display_amount, display_unit) { 258 draft.display_price = Some(RadrootsCoreMoney { 259 amount: display_amount, 260 currency: money_currency, 261 }); 262 draft.display_price_unit = Some(display_unit); 263 } 264 upsert_bin(bin_id, draft); 265 } 266 267 let bins: Vec<RadrootsListingBin> = bin_order 268 .iter() 269 .filter_map(|bin_id| bin_drafts.get(bin_id).map(|draft| (bin_id, draft))) 270 .filter_map(|(bin_id, draft)| { 271 let quantity = draft.quantity.clone()?; 272 let price_per_canonical_unit = draft.price_per_canonical_unit.clone()?; 273 Some(RadrootsListingBin { 274 bin_id: bin_id.clone(), 275 quantity, 276 price_per_canonical_unit, 277 display_amount: draft.display_amount, 278 display_unit: draft.display_unit, 279 display_label: draft.display_label.clone(), 280 display_price: draft.display_price.clone(), 281 display_price_unit: draft.display_price_unit, 282 }) 283 }) 284 .collect(); 285 if bins.is_empty() { 286 return Err(RadrootsListingEventIndexError::ParseError); 287 } 288 289 let primary_bin_id = required(get_first("radroots:primary_bin"))? 290 .trim() 291 .to_string(); 292 if primary_bin_id.is_empty() { 293 return Err(RadrootsListingEventIndexError::ParseError); 294 } 295 if !bins.iter().any(|bin| bin.bin_id == primary_bin_id) { 296 return Err(RadrootsListingEventIndexError::ParseError); 297 } 298 299 let mut primary: Option<String> = None; 300 let mut city: Option<String> = None; 301 let mut region: Option<String> = None; 302 let mut country: Option<String> = None; 303 if let Some(t) = location_tags.last() { 304 if t.len() >= 2 { 305 primary = Some(t[1].clone()); 306 } 307 if t.len() >= 3 { 308 city = Some(t[2].clone()); 309 } 310 if t.len() >= 4 { 311 region = Some(t[3].clone()); 312 } 313 if t.len() >= 5 { 314 country = Some(t[4].clone()); 315 } 316 } 317 318 let geohash = tags 319 .iter() 320 .filter(|t| t.first().map(|k| k == "g").unwrap_or(false)) 321 .filter_map(|t| t.get(1).cloned()) 322 .max_by_key(|s| s.len()); 323 324 let mut lat: Option<f64> = None; 325 let mut lng: Option<f64> = None; 326 for t in tags.iter().filter(|t| { 327 t.first() 328 .map(|k| k.eq_ignore_ascii_case("l")) 329 .unwrap_or(false) 330 }) { 331 if t.len() >= 3 { 332 let val = t[1].parse::<f64>().ok(); 333 let label = t[2].as_str(); 334 match label { 335 "dd.lat" => lat = val, 336 "dd.lon" => lng = val, 337 _ => {} 338 } 339 } 340 } 341 342 let location = if primary.is_some() 343 || city.is_some() 344 || region.is_some() 345 || country.is_some() 346 || lat.is_some() 347 || lng.is_some() 348 || geohash.is_some() 349 { 350 Some(RadrootsListingLocation { 351 primary: primary.unwrap_or_default(), 352 city, 353 region, 354 country, 355 lat, 356 lng, 357 geohash, 358 }) 359 } else { 360 None 361 }; 362 363 let images = tags 364 .iter() 365 .filter(|t| t.first().map(|k| k == "image").unwrap_or(false)) 366 .map(|t| { 367 let url = t.get(1).cloned().unwrap_or_default(); 368 let size = if t.len() >= 3 { 369 let mut parts = t[2].split('x'); 370 let w = parts.next().and_then(|v| v.parse::<u32>().ok()); 371 let h = parts.next().and_then(|v| v.parse::<u32>().ok()); 372 if parts.next().is_none() { 373 match (w, h) { 374 (Some(w), Some(h)) => Some(RadrootsListingImageSize { w, h }), 375 _ => None, 376 } 377 } else { 378 None 379 } 380 } else { 381 None 382 }; 383 RadrootsListingImage { url, size } 384 }) 385 .collect::<Vec<_>>(); 386 let images = if images.is_empty() { None } else { Some(images) }; 387 388 let inventory_available = get_first("inventory") 389 .and_then(|value| parse_decimal(&value)); 390 391 let availability = if let Some(value) = get_first("status") 392 .map(|v| v.trim().to_string()) 393 .filter(|v| !v.is_empty()) 394 { 395 let status = match value.as_str() { 396 "active" => RadrootsListingStatus::Active, 397 "sold" => RadrootsListingStatus::Sold, 398 _ => RadrootsListingStatus::Other { value }, 399 }; 400 Some(RadrootsListingAvailability::Status { status }) 401 } else { 402 let start = get_first("published_at").and_then(|v| v.parse::<u64>().ok()); 403 let end = get_first("expires_at").and_then(|v| v.parse::<u64>().ok()); 404 if start.is_some() || end.is_some() { 405 Some(RadrootsListingAvailability::Window { start, end }) 406 } else { 407 None 408 } 409 }; 410 411 let delivery_method = tags 412 .iter() 413 .find(|t| t.first().map(|k| k == "delivery").unwrap_or(false)) 414 .and_then(|t| t.get(1).map(|v| v.trim().to_string())) 415 .and_then(|kind| { 416 if kind.is_empty() { 417 return None; 418 } 419 let method = match kind.as_str() { 420 "pickup" => RadrootsListingDeliveryMethod::Pickup, 421 "local_delivery" => RadrootsListingDeliveryMethod::LocalDelivery, 422 "shipping" => RadrootsListingDeliveryMethod::Shipping, 423 "other" => { 424 let detail = tags 425 .iter() 426 .find(|t| t.first().map(|k| k == "delivery").unwrap_or(false)) 427 .and_then(|t| t.get(2)) 428 .map(|v| v.trim().to_string()) 429 .filter(|v| !v.is_empty())?; 430 RadrootsListingDeliveryMethod::Other { method: detail } 431 } 432 _ => return None, 433 }; 434 Some(method) 435 }); 436 437 let mut discounts: Vec<RadrootsCoreDiscount> = Vec::new(); 438 for t in tags 439 .iter() 440 .filter(|t| t.first().map(|k| k == "radroots:discount").unwrap_or(false)) 441 { 442 if let Some(payload) = t.get(1) { 443 if let Ok(discount) = serde_json::from_str::<RadrootsCoreDiscount>(payload) { 444 discounts.push(discount); 445 } 446 } 447 } 448 let discounts = if discounts.is_empty() { None } else { Some(discounts) }; 449 450 Ok(RadrootsListing { 451 d_tag, 452 farm, 453 product, 454 primary_bin_id, 455 bins, 456 resource_area, 457 plot, 458 discounts, 459 inventory_available, 460 availability, 461 delivery_method, 462 location, 463 images, 464 }) 465 } 466 467 fn create_radroots_listing_event_metadata( 468 id: String, 469 author: String, 470 published_at: u32, 471 kind: u32, 472 tags: &[Vec<String>], 473 ) -> Result<RadrootsListingEventMetadata, RadrootsListingEventIndexError> { 474 let listing = parse_listing_from_tags(tags)?; 475 Ok(RadrootsListingEventMetadata { 476 id, 477 author, 478 published_at, 479 kind, 480 listing, 481 }) 482 } 483 484 pub trait ToRadrootsListingEventIndex { 485 fn to_radroots_listing_event( 486 &self, 487 ) -> Result<RadrootsListingEventIndex, RadrootsListingEventIndexError>; 488 } 489 490 impl ToRadrootsListingEventIndex for RelayIndexerEvent { 491 fn to_radroots_listing_event( 492 &self, 493 ) -> Result<RadrootsListingEventIndex, RadrootsListingEventIndexError> { 494 let kind_u32 = self.kind.as_u64() as u32; 495 let id = self.id.clone(); 496 let author = self.author.clone(); 497 498 let metadata = create_radroots_listing_event_metadata( 499 id.clone(), 500 author.clone(), 501 self.created_at, 502 kind_u32, 503 &self.tags, 504 )?; 505 506 Ok(RadrootsListingEventIndex { 507 event: RadrootsNostrEvent { 508 id, 509 author, 510 created_at: self.created_at, 511 kind: kind_u32, 512 tags: self.tags.clone(), 513 content: self.content.clone(), 514 sig: self.sig.clone(), 515 }, 516 metadata, 517 }) 518 } 519 }