tangle_indexer


git clone https://radroots.dev/git/tangle_indexer.git
Log | Files | Refs | Submodules | LICENSE

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 }