Skip to main content

nmp_threading/
etag.rs

1//! Kind-blind resolver for marked/positional `e` tag reply/root grammar.
2//!
3//! The resolver intentionally ignores event kind. It interprets the same
4//! marked/positional `e` tag shape for any caller-supplied event scope, so
5//! NIP-29 group timelines, note feeds, or other scoped event streams can reuse
6//! one threading read model without app-side tag parsing.
7
8use nmp_core::substrate::KernelEvent;
9
10use crate::{ParentResolver, ThreadPointer};
11
12/// [`ParentResolver`] over raw `e` tags, independent of event kind.
13#[derive(Clone, Copy, Debug, Default)]
14pub struct EtagThreadResolver;
15
16impl ParentResolver for EtagThreadResolver {
17    fn parent(&self, event: &KernelEvent) -> Option<ThreadPointer> {
18        parse_e_refs(&event.tags)
19            .reply
20            .map(|r| ThreadPointer::Event {
21                id: r.id,
22                relay: r.relay,
23                kind: None,
24            })
25    }
26
27    fn root(&self, event: &KernelEvent) -> Option<ThreadPointer> {
28        parse_e_refs(&event.tags)
29            .root
30            .map(|r| ThreadPointer::Event {
31                id: r.id,
32                relay: r.relay,
33                kind: None,
34            })
35    }
36
37    fn parent_author(&self, event: &KernelEvent) -> Option<String> {
38        event
39            .tags
40            .iter()
41            .find(|tag| tag.first().map(String::as_str) == Some("p"))
42            .and_then(|tag| tag.get(1).cloned())
43    }
44}
45
46#[derive(Clone)]
47struct ERef {
48    id: String,
49    relay: Option<String>,
50    marker: Option<String>,
51}
52
53#[derive(Default)]
54struct ERefs {
55    root: Option<ERef>,
56    reply: Option<ERef>,
57}
58
59fn parse_e_refs(tags: &[Vec<String>]) -> ERefs {
60    let e_tags: Vec<&Vec<String>> = tags
61        .iter()
62        .filter(|tag| tag.first().map(String::as_str) == Some("e"))
63        .collect();
64    let has_marker = e_tags.iter().any(|tag| {
65        matches!(
66            tag.get(3).map(String::as_str),
67            Some("root" | "reply" | "mention")
68        )
69    });
70
71    if has_marker {
72        let mut refs = ERefs::default();
73        for tag in e_tags {
74            let Some(eref) = e_ref(tag) else { continue };
75            match eref.marker.as_deref() {
76                Some("root") if refs.root.is_none() => refs.root = Some(eref),
77                Some("reply") if refs.reply.is_none() => refs.reply = Some(eref),
78                _ => {}
79            }
80        }
81        if refs.reply.is_none() {
82            refs.reply = refs.root.clone();
83        }
84        return refs;
85    }
86
87    let resolved: Vec<ERef> = e_tags.into_iter().filter_map(|tag| e_ref(tag)).collect();
88    match resolved.len() {
89        0 => ERefs::default(),
90        1 => ERefs {
91            root: Some(resolved[0].clone()),
92            reply: Some(resolved[0].clone()),
93        },
94        n => ERefs {
95            root: Some(resolved[0].clone()),
96            reply: Some(resolved[n - 1].clone()),
97        },
98    }
99}
100
101fn e_ref(tag: &[String]) -> Option<ERef> {
102    let id = tag.get(1)?.clone();
103    if id.is_empty() {
104        return None;
105    }
106    Some(ERef {
107        id,
108        relay: tag.get(2).filter(|s| !s.is_empty()).cloned(),
109        marker: tag.get(3).filter(|s| !s.is_empty()).cloned(),
110    })
111}