use super::following::FolloweesManager;
use super::prelude::*;
use super::util::subnn;
use crate::niri::{FromNamedNode, ToNamedNode};
use nostr::{Filter, Kind, SubscriptionId, Timestamp};
use oxigraph::model::vocab::rdf;
use std::time::Duration;
#[derive(Clone)]
struct FeedsQuery;
pub trait CustomFeeds {
fn feed_node(&self, pubk: &PublicKey, name: Option<&str>) -> NamedNode;
fn feed_title(
&self,
feed_node: NamedNodeRef,
) -> Result<String, RdfStoreError>;
fn feed_setup(
&self,
pubk: &PublicKey,
name: &str,
title: &str,
feed_emoji: &str,
) -> Result<NamedNode, 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_follow_pubkey(
&self,
feed_node: NamedNodeRef,
pubkey: &PublicKey,
) -> Result<bool, RdfStoreError>;
fn feed_followees(
&self,
feed_node: NamedNodeRef,
) -> Result<Vec<PublicKey>, RdfStoreError>;
fn feed_unfollow_pubkey(
&self,
feed_node: NamedNodeRef,
pubkey: &PublicKey,
) -> Result<bool, 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>;
}
const TYPE_FEED: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed");
const PRED_FEED_TITLE: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#title");
const PRED_FEED_OWNER: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#owner");
const PRED_FEED_OWNER_PUBKEY: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#owner_pubkey");
pub const PRED_FEED_FILTER_AUTHOR: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#allow_author");
pub const PRED_FEED_FILTER_AUTHOR_PUBKEY: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked(
"http://nostralink.org/Feed#allow_author_pubkey",
);
pub const PRED_FEED_FILTER_HASHTAG: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#allow_hashtag");
pub const PRED_FEED_FILTER_FOLLOW_PACK: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#allow_followpack");
#[allow(dead_code)]
pub const PRED_FEED_FILTER_MENTIONED: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#filter_mentioned");
#[allow(dead_code)]
pub const PRED_FEED_FILTER_KIND: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#filter_kind");
#[allow(dead_code)]
const PRED_FEED_ICON: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#icon");
const PRED_FEED_EMOJI: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#emoji");
const PRED_FEED_EVENT: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("http://nostralink.org/Feed#event");
#[deprecated]
#[allow(dead_code)]
const PRED_FOLLOWS_HASHTAG: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("https://w3id.org/nostr#follows_hashtag");
const PRED_WATCH_REGEXP_I: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("https://w3id.org/nostr#watches_regexp_i");
#[allow(dead_code)]
const PRED_WATCH_REGEXP_S: NamedNodeRef<'static> =
NamedNodeRef::new_unchecked("https://w3id.org/nostr#watches_regexp_s");
impl CustomFeeds for RdfEventsStore {
fn feed_node(&self, pubk: &PublicKey, name: Option<&str>) -> NamedNode {
let path = match name {
Some(n) => &format!("/feeds/{n}"),
None => "/feeds/default",
};
pubk.named_node_with(path, None)
.unwrap_or(NamedNode::new_unchecked("urn:feeds:nostralink"))
}
fn feed_setup(
&self,
pubk: &PublicKey,
name: &str,
feed_title: &str,
feed_emoji: &str,
) -> Result<NamedNode, RdfStoreError> {
let node = self.feed_node(pubk, Some(name));
self.store.insert(QuadRef::new(
node.as_ref(),
rdf::TYPE,
TYPE_FEED,
&GraphName::DefaultGraph,
))?;
self.store.insert(QuadRef::new(
node.as_ref(),
PRED_FEED_OWNER,
pubk.named_node()?.as_ref(),
&GraphName::DefaultGraph,
))?;
self.store.insert(QuadRef::new(
node.as_ref(),
PRED_FEED_OWNER_PUBKEY,
&Literal::from(pubk.to_hex()),
&GraphName::DefaultGraph,
))?;
self.store.insert(QuadRef::new(
node.as_ref(),
PRED_FEED_TITLE,
&Literal::from(feed_title),
&GraphName::DefaultGraph,
))?;
self.store.insert(QuadRef::new(
node.as_ref(),
PRED_FEED_EMOJI,
&Literal::from(feed_emoji),
&GraphName::DefaultGraph,
))?;
Ok(node)
}
fn feed_title(
&self,
feed_node: NamedNodeRef,
) -> Result<String, RdfStoreError> {
self.string_for_subject_predicate(feed_node, PRED_FEED_TITLE)
}
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> {
let mut subs = Vec::new();
if let Some(pk) = pubk {
subs.push(subnn("owner_nip21", pk.named_node()?)?);
}
let results: Arc<TRdfResultSet<FeedsQuery>> = self
.run_query_typed(&nrq_get("feeds")?, None, subs, None)
.map_err(|_| RdfStoreError::QueryError)?;
Ok(results
.rows
.iter()
.map(|row| {
let feed = row.get_node("feed").unwrap();
let title = row.get_string("title").unwrap();
(feed, title)
})
.collect())
}
fn feed_filters(
&self,
node: NamedNodeRef,
) -> Vec<(Filter, SubscriptionId)> {
let mut filters = Vec::new();
if let Ok(hashtags) = self.feed_followed_hashtags(node) {
if !hashtags.is_empty() {
filters.push((
Filter::new()
.kind(Kind::TextNote)
.kind(Kind::LongFormTextNote)
.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_FEED_FILTER_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_FEED_FILTER_HASHTAG,
&Literal::from(hashtag),
&GraphName::DefaultGraph,
))?;
Ok(())
}
fn feed_followees(
&self,
feed_node: NamedNodeRef,
) -> Result<Vec<PublicKey>, RdfStoreError> {
Ok(self
.store
.quads_for_pattern(
Some(feed_node.into()),
Some(PRED_FEED_FILTER_AUTHOR),
None,
None,
)
.filter_map(|quad| match &quad.ok()?.object {
Term::NamedNode(node) => node.from_named_node().ok(),
_ => None,
})
.collect())
}
fn feed_follow_pubkey(
&self,
feed_node: NamedNodeRef,
pubkey: &PublicKey,
) -> Result<bool, RdfStoreError> {
let feed_owner_pubk = self
.store
.quads_for_pattern(
Some(feed_node.into()),
Some(PRED_FEED_OWNER),
None,
None,
)
.filter_map(|quad| match &quad.ok()?.object {
Term::NamedNode(node) => node.from_named_node().ok(),
_ => None,
})
.next()
.ok_or("")
.map_err(|_| RdfStoreError::QuadError)?;
self.follow(feed_owner_pubk, *pubkey)?;
self.store
.insert(QuadRef::new(
feed_node,
PRED_FEED_FILTER_AUTHOR,
pubkey.named_node()?.as_ref(),
&GraphName::DefaultGraph,
))
.map_err(|_| RdfStoreError::QuadError)?;
self.store
.insert(QuadRef::new(
feed_node,
PRED_FEED_FILTER_AUTHOR_PUBKEY,
&Literal::from(pubkey.to_hex()),
&GraphName::DefaultGraph,
))
.map_err(|_| RdfStoreError::QuadError)
}
fn feed_unfollow_pubkey(
&self,
feed_node: NamedNodeRef,
pubkey: &PublicKey,
) -> Result<bool, RdfStoreError> {
self.store
.remove(QuadRef::new(
feed_node,
PRED_FEED_FILTER_AUTHOR,
pubkey.named_node()?.as_ref(),
&GraphName::DefaultGraph,
))
.map_err(|_| RdfStoreError::QuadError)?;
self.store
.remove(QuadRef::new(
feed_node,
PRED_FEED_FILTER_AUTHOR_PUBKEY,
&Literal::from(pubkey.to_hex()),
&GraphName::DefaultGraph,
))
.map_err(|_| RdfStoreError::QuadError)
}
fn feed_followed_hashtags(
&self,
feed_node: NamedNodeRef,
) -> Result<Vec<String>, RdfStoreError> {
Ok(self
.store
.quads_for_pattern(
Some(feed_node.into()),
Some(PRED_FEED_FILTER_HASHTAG),
None,
None,
)
.filter_map(|quad| match &quad.ok()?.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_FEED_FILTER_HASHTAG),
Some((&Literal::from(hashtag)).into()),
None,
)
.filter_map(|quad| match &quad.ok()?.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_FEED_FILTER_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(())
}
}