nostralink 0.2.4

Linked data library for nostr
Documentation
//! Helpers to build sparql substitutions

use super::manager::{RdfEventsStore, RdfStoreError};
use super::vars as SPVars;
use crate::err::LDError;
use crate::niri::ToNamedNode;
use derive_builder::Builder;
use language::Language;
use nostr::{EventId, PublicKey, Timestamp};
use oxigraph::model::*;
use std::time::Duration;

impl RdfEventsStore {
    /// Returns the Literal for the first object matching this subject and predicate
    pub fn literal_for_subject_predicate(
        &self,
        subject_node: NamedNodeRef,
        pred: NamedNodeRef,
    ) -> Result<Literal, RdfStoreError> {
        self.store
            .quads_for_pattern(
                Some(subject_node.into()),
                Some(pred),
                None,
                None,
            )
            .filter_map(|quad| match &quad.ok()?.object {
                Term::Literal(lit) => Some(lit.clone()),
                _ => None,
            })
            .next()
            .ok_or("No quad found")
            .map_err(|_| RdfStoreError::QuadError)
    }

    /// Returns the string value for the first object matching this subject and predicate
    pub fn string_for_subject_predicate(
        &self,
        subject_node: NamedNodeRef,
        pred: NamedNodeRef,
    ) -> Result<String, RdfStoreError> {
        self.literal_for_subject_predicate(subject_node, pred)
            .map(|l| l.to_string())
    }

    /// Remove from the store quads matching a certain quads pattern
    /// Returns the number of removed quads
    pub fn rm_quads_pattern(
        &self,
        subject: Option<SubjectRef<'_>>,
        predicate: Option<NamedNodeRef<'_>>,
        object: Option<TermRef<'_>>,
        graph_name: Option<GraphNameRef<'_>>,
    ) -> Result<u32, RdfStoreError> {
        let mut removed: u32 = 0;

        for q in self
            .store
            .quads_for_pattern(subject, predicate, object, graph_name).flatten()
        {
            self.store.remove(&q)?;
            removed += 1;
        }

        Ok(removed)
    }
}

pub fn subl(
    varname: &str,
    literal: Literal,
) -> Result<(Variable, Term), LDError> {
    Ok((
        Variable::new(varname).map_err(|_| LDError::SubstitutionError)?,
        literal.into(),
    ))
}

pub fn subnn(
    varname: &str,
    nn: NamedNode,
) -> Result<(Variable, Term), LDError> {
    Ok((
        Variable::new(varname).map_err(|_| LDError::SubstitutionError)?,
        nn.into(),
    ))
}

#[derive(Builder, Debug, Default)]
#[builder(default)]
pub struct QuerySubs {
    #[builder(setter(strip_option))]
    event_id: Option<EventId>,
    #[builder(setter(strip_option))]
    replies_to_event_id: Option<EventId>,
    #[builder(setter(strip_option))]
    in_forum: Option<EventId>,
    #[builder(setter(strip_option))]
    since: Option<Duration>,
    #[builder(setter(strip_option))]
    author: Option<PublicKey>,
    #[builder(setter(strip_option))]
    notifications_for: Option<PublicKey>,
    #[builder(setter(strip_option))]
    identity_nip21: Option<PublicKey>,
    #[builder(setter(strip_option))]
    nip21: Option<PublicKey>,
    #[builder(setter(strip_option))]
    lang: Option<Language>,
    #[builder(setter(strip_option))]
    hashtag: Option<String>,
    #[builder(setter(strip_option))]
    only_followees: Option<i16>,
    #[builder(setter(strip_option))]
    feed: Option<NamedNode>,
    #[builder(setter(strip_option))]
    trust_score_min: Option<f32>,
}

impl QuerySubs {
    #[deprecated]
    pub fn subs(&self) -> Result<Vec<(Variable, Term)>, LDError> {
        let mut subs_v = Vec::new();
        if let Some(id) = self.event_id {
            subs_v.push(subl(SPVars::EVENT_ID, id.to_hex().into())?);
        }
        Ok(subs_v)
    }

    pub fn subs_with_ckey(
        &self,
    ) -> Result<(Vec<(Variable, Term)>, String), LDError> {
        let mut cache_key = String::new();
        let mut subs_v = Vec::new();

        if let Some(pk) = self.identity_nip21 {
            subs_v.push(subnn("identity_nip21", pk.named_node().unwrap())?);
        }

        if let Some(pk) = self.nip21 {
            subs_v.push(subnn("nip21", pk.named_node().unwrap())?);
            cache_key.push_str(&format!("nip21_{}", pk.to_hex()));
        }

        if let Some(pk) = self.notifications_for {
            subs_v.push(subnn("notifications_for", pk.named_node().unwrap())?);
            cache_key.push_str(&format!("notif_{}", pk.to_hex()));
        }

        if let Some(id) = self.event_id {
            subs_v.push(subl(SPVars::EVENT_ID, id.to_hex().into())?);
            cache_key.push_str(&format!("ev_{}", id.to_hex()));
        }

        if let Some(ev_id) = self.replies_to_event_id {
            subs_v.push(subl("e_tag_value", Literal::from(ev_id.to_hex()))?);
            subs_v.push(subl("filter_e_tag", Literal::from(1))?);
            cache_key.push_str(&format!("_etag_{}", ev_id.to_hex()));
        } else {
            subs_v.push(subl("filter_e_tag", Literal::from(0))?);
        }

        if let Some(since_dur) = self.since {
            let created_since = Timestamp::now() - since_dur;
            subs_v.push(subl(
                "created_at_since",
                Literal::from(created_since.as_secs()),
            )?);
            cache_key.push_str(&format!("since_{}", since_dur.as_secs()));
        }

        if let Some(score) = self.trust_score_min {
            subs_v.push(subl(
                "trust_score_min",
                Literal::from(score),
            )?);
            cache_key.push_str(&format!("tsm_{}", score));
        }

        if let Some(ref hashtag) = self.hashtag {
            subs_v.push(subl("query_hashtag", Literal::from(hashtag.clone()))?);
            cache_key.push_str(&format!("_ht{}", hashtag));
        }

        if let Some(lang) = self.lang {
            let lang_tag = lang.as_tag();

            subs_v.push(subl(
                "lang_tag_filter",
                Literal::from(lang_tag.to_string()),
            )?);
        } else {
            subs_v.push(subl("lang_tag_filter", Literal::from(""))?);
        }

        if let Some(event_id) = &self.in_forum {
            subs_v.push(subl("f_tag_value", Literal::from(event_id.to_hex()))?);
            subs_v.push(subl("filter_f_tag", Literal::from(1))?);
            cache_key.push_str(&format!("_ftag_{}", event_id.to_hex()));
        } else {
            subs_v.push(subl("filter_f_tag", Literal::from(0))?);
        }

        if let Some(only_f) = self.only_followees {
            subs_v.push(subl("only_followees", Literal::from(only_f))?);
        }

        if let Some(feed_node) = &self.feed {
            subs_v.push(subl("feed_mode", Literal::from(1))?);
            subs_v.push(subnn("feed", feed_node.clone())?);
            cache_key.push_str(&format!("_feed_{}", feed_node));
        }

        Ok((subs_v, cache_key))
    }
}

#[derive(Builder, Debug, Default)]
#[builder(default)]
pub struct SearchQuerySubs<'a> {
    #[builder(setter(strip_option))]
    event_id: Option<EventId>,
    #[builder(setter(strip_option))]
    since_ts: Option<Timestamp>,
    #[builder(setter(strip_option))]
    since_duration: Option<Duration>,
    #[builder(setter(strip_option))]
    author_name: Option<&'a str>,
    #[builder(setter(strip_option))]
    nip21: Option<PublicKey>,
    #[builder(setter(strip_option))]
    feed: Option<NamedNode>,
    #[builder(setter(strip_option))]
    content_search_regex: Option<&'a str>,
}

impl<'a> SearchQuerySubs<'a> {
    pub fn subs_with_ckey(
        &self,
    ) -> Result<(Vec<(Variable, Term)>, String), LDError> {
        let mut cache_key = String::from("search_");
        let mut subs_v = Vec::new();

        if let Some(regex) = self.content_search_regex {
            subs_v.push(subl("content_search_regex", Literal::from(regex))?);
            cache_key.push_str(regex);
        }

        if let Some(name) = self.author_name {
            subs_v.push(subl("author_name", Literal::from(name))?);
            cache_key.push_str(name);
        }

        if let Some(pk) = self.nip21 {
            subs_v.push(subnn("nip21", pk.named_node().unwrap())?);
            cache_key.push_str(&format!("nip21_{}", pk.to_hex()));
        }

        if let Some(since_ts) = self.since_ts {
            subs_v.push(subl(
                "created_at_since",
                Literal::from(since_ts.as_secs()),
            )?);
            cache_key.push_str(&format!("since_{}", since_ts.as_secs()));
        }

        if let Some(since_dur) = self.since_duration {
            let created_since = Timestamp::now() - since_dur;
            subs_v.push(subl(
                "created_at_since",
                Literal::from(created_since.as_secs()),
            )?);
            cache_key.push_str(&format!("since_{}", since_dur.as_secs()));
        }

        Ok((subs_v, cache_key))
    }
}

#[test]
fn test_subs() -> Result<(), LDError> {
    let (_, _) = QuerySubsBuilder::default()
        .event_id(EventId::parse("5d35413cad64e6c4e9cd9788b1d82b935744a36582239d753cd4dadca045580a").unwrap())
        .lang(Language::EnGb)
        .build().map_err(|_| LDError::EventBuildError)?
        .subs_with_ckey()?;

    Ok(())
}