nostralink 0.2.3

Linked data library for nostr
Documentation
//! Customized feeds API

use super::prelude::*;
use nostr::{Filter, Kind, SubscriptionId, Timestamp};
use std::time::Duration;

pub trait CustomFeeds {
    fn feed_node(&self, pubk: &PublicKey, name: Option<&str>) -> NamedNode;

    fn feed_init(
        &self,
        node: NamedNodeRef,
        title: Option<&str>,
    ) -> Result<(), RdfStoreError>;

    fn feed_attach_event(
        &self,
        feed_node: NamedNodeRef,
        event_node: NamedNodeRef,
    ) -> Result<(), RdfStoreError>;

    fn feed_filters(&self, node: NamedNodeRef)
        -> Vec<(Filter, SubscriptionId)>;

    fn feeds_list(
        &self,
        pubk: Option<&PublicKey>,
    ) -> Result<Vec<(NamedNode, String)>, RdfStoreError>;

    fn feed_follow_hashtag(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<(), RdfStoreError>;

    fn feed_unfollow_hashtag(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<(), RdfStoreError>;

    fn feed_followed_hashtags(
        &self,
        feed_node: NamedNodeRef,
    ) -> Result<Vec<String>, RdfStoreError>;

    fn feeds_following_hashtag(
        &self,
        hashtag: &str,
    ) -> Result<Vec<NamedNode>, RdfStoreError>;

    fn feed_hashtag_is_followed(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<bool, RdfStoreError>;

    fn feed_watch_regexp(
        &self,
        feed_node: NamedNodeRef,
        regexp: &str,
    ) -> Result<(), RdfStoreError>;
}

/// Feed title predicate
const PRED_FEED_TITLE: NamedNodeRef<'static> =
    NamedNodeRef::new_unchecked("https://w3id.org/nostr#feed_title");

const PRED_FEED_EVENT: NamedNodeRef<'static> =
    NamedNodeRef::new_unchecked("https://w3id.org/nostr#feed_event");

/// Follow hashtag predicate
const PRED_FOLLOWS_HASHTAG: NamedNodeRef<'static> =
    NamedNodeRef::new_unchecked("https://w3id.org/nostr#follows_hashtag");

/// Watch regexp predicate (case-insensitive)
const PRED_WATCH_REGEXP_I: NamedNodeRef<'static> =
    NamedNodeRef::new_unchecked("https://w3id.org/nostr#watches_regexp_i");

/// Watch regexp predicate (case-sensitive)
#[allow(dead_code)]
const PRED_WATCH_REGEXP_S: NamedNodeRef<'static> =
    NamedNodeRef::new_unchecked("https://w3id.org/nostr#watches_regexp_s");

impl CustomFeeds for RdfEventsStore {
    /// Return the NamedNode for a pubk and feed name
    fn feed_node(&self, pubk: &PublicKey, name: Option<&str>) -> NamedNode {
        pubk.named_node_with_f(name.unwrap_or("feed-default"))
            .unwrap_or(NamedNode::new_unchecked("urn:feeds:nostralink"))
    }

    fn feed_init(
        &self,
        node: NamedNodeRef,
        feed_title: Option<&str>,
    ) -> Result<(), RdfStoreError> {
        if let Some(title) = feed_title {
            self.store.insert(QuadRef::new(
                node,
                PRED_FEED_TITLE,
                &Literal::from(title),
                &GraphName::DefaultGraph,
            ))?;
        }

        Ok(())
    }

    fn feed_attach_event(
        &self,
        feed_node: NamedNodeRef,
        event_node: NamedNodeRef,
    ) -> Result<(), RdfStoreError> {
        self.store.insert(QuadRef::new(
            event_node,
            PRED_FEED_EVENT,
            feed_node,
            &GraphName::DefaultGraph,
        ))?;

        Ok(())
    }

    fn feeds_list(
        &self,
        _pubk: Option<&PublicKey>,
    ) -> Result<Vec<(NamedNode, String)>, RdfStoreError> {
        Ok(self
            .store
            .quads_for_pattern(None, Some(PRED_FEED_TITLE), None, None)
            .collect::<Result<Vec<_>, _>>()?
            .iter()
            .filter_map(|quad| match &quad.object {
                Term::Literal(title) => match &quad.subject {
                    Subject::NamedNode(node) => {
                        Some((node.clone(), title.value().to_string()))
                    }
                    _ => None,
                },
                _ => None,
            })
            .collect())
    }

    fn feed_filters(
        &self,
        node: NamedNodeRef,
    ) -> Vec<(Filter, SubscriptionId)> {
        let mut filters = Vec::new();

        if let Ok(hashtags) = self.feed_followed_hashtags(node) {
            filters.push((
                Filter::new().kind(Kind::TextNote).hashtags(hashtags).since(
                    Timestamp::now() - Duration::from_secs(3600 * 24 * 3),
                ),
                SubscriptionId::new(node.to_string()),
            ));
        }

        filters
    }

    fn feed_follow_hashtag(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<(), RdfStoreError> {
        self.store.insert(QuadRef::new(
            feed_node,
            PRED_FOLLOWS_HASHTAG,
            &Literal::from(hashtag),
            &GraphName::DefaultGraph,
        ))?;

        Ok(())
    }

    fn feed_unfollow_hashtag(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<(), RdfStoreError> {
        self.store.remove(QuadRef::new(
            feed_node,
            PRED_FOLLOWS_HASHTAG,
            &Literal::from(hashtag),
            &GraphName::DefaultGraph,
        ))?;

        Ok(())
    }

    fn feed_followed_hashtags(
        &self,
        feed_node: NamedNodeRef,
    ) -> Result<Vec<String>, RdfStoreError> {
        Ok(self
            .store
            .quads_for_pattern(
                Some(feed_node.into()),
                Some(PRED_FOLLOWS_HASHTAG),
                None,
                None,
            )
            .collect::<Result<Vec<_>, _>>()
            .map_err(|_| RdfStoreError::QueryError)?
            .into_iter()
            .filter_map(|quad| match &quad.object {
                Term::Literal(hashtag) => Some(hashtag.value().to_string()),
                _ => None,
            })
            .collect())
    }

    fn feeds_following_hashtag(
        &self,
        hashtag: &str,
    ) -> Result<Vec<NamedNode>, RdfStoreError> {
        Ok(self
            .store
            .quads_for_pattern(
                None,
                Some(PRED_FOLLOWS_HASHTAG),
                Some((&Literal::from(hashtag)).into()),
                None,
            )
            .collect::<Result<Vec<_>, _>>()
            .map_err(|_| RdfStoreError::QueryError)?
            .into_iter()
            .filter_map(|quad| match &quad.subject {
                Subject::NamedNode(node) => Some(node.clone()),
                _ => None,
            })
            .collect())
    }

    fn feed_hashtag_is_followed(
        &self,
        feed_node: NamedNodeRef,
        hashtag: &str,
    ) -> Result<bool, RdfStoreError> {
        Ok(!self
            .store
            .quads_for_pattern(
                Some(feed_node.into()),
                Some(PRED_FOLLOWS_HASHTAG),
                Some((&Literal::from(hashtag)).into()),
                None,
            )
            .collect::<Result<Vec<_>, _>>()?
            .is_empty())
    }

    fn feed_watch_regexp(
        &self,
        feed_node: NamedNodeRef,
        regexp: &str,
    ) -> Result<(), RdfStoreError> {
        self.store.insert(QuadRef::new(
            feed_node,
            PRED_WATCH_REGEXP_I,
            &Literal::from(regexp),
            &GraphName::DefaultGraph,
        ))?;

        Ok(())
    }
}