use super::*;
pub(in crate::core) const RELAY_MESSAGE_AUTHOR_CHUNK_SIZE: usize =
nostr_double_ratchet::MESSAGE_AUTHOR_FILTER_CHUNK_SIZE;
pub(in crate::core) const MESH_MESSAGE_AUTHOR_CHUNK_SIZE: usize = 500;
const MESSAGE_AUTHOR_SUBSCRIPTION_PREFIX: &str = "ndr-protocol-messages-";
pub(in crate::core) fn message_author_subscription_filters(
authors: impl IntoIterator<Item = PublicKey>,
chunk_size: usize,
) -> Vec<Filter> {
nostr_double_ratchet::build_message_author_subscription_filters(
authors,
chunk_size,
nostr_double_ratchet::MESSAGE_AUTHOR_SUBSCRIPTION_LIMIT,
)
.filters
}
fn is_message_author_filter(filter: &Filter) -> bool {
filter.authors.is_some()
&& filter.ids.is_none()
&& filter.generic_tags.is_empty()
&& filter.kinds.as_ref().is_some_and(|kinds| {
kinds.len() == 1 && kinds.contains(&Kind::from(MESSAGE_EVENT_KIND as u16))
})
}
pub(in crate::core) fn split_message_author_filters(
filters: Vec<Filter>,
) -> (Vec<Filter>, Vec<Filter>) {
filters
.into_iter()
.partition(|filter| !is_message_author_filter(filter))
}
pub(super) fn is_protocol_subscription_id(id: &str) -> bool {
id == PROTOCOL_SUBSCRIPTION_ID || id.starts_with(MESSAGE_AUTHOR_SUBSCRIPTION_PREFIX)
}
pub(super) async fn evict_protocol_subscriptions(relay: &nostr_sdk::Relay) {
for id in relay.subscriptions().await.into_keys() {
if is_protocol_subscription_id(id.as_str()) {
let _ = relay.unsubscribe(&id).await;
}
}
}
pub(super) async fn subscribe_protocol_filters(
client: &Client,
filters: Vec<Filter>,
) -> Result<(), String> {
let (shared, chunks) = split_message_author_filters(filters);
let shared = subscribe_protocol_filters_with_id(
client,
SubscriptionId::new(PROTOCOL_SUBSCRIPTION_ID),
shared,
)
.await;
let chunks = subscribe_message_author_chunks(client, chunks).await;
shared.and(chunks)
}
async fn subscribe_message_author_chunks(
client: &Client,
chunks: Vec<Filter>,
) -> Result<(), String> {
let mut attempted = 0usize;
let mut accepted = 0usize;
let mut last_error = None;
for relay in client.relays().await.values() {
let mut missing = chunks.iter().collect::<Vec<_>>();
let mut used = HashSet::new();
for (id, existing) in relay.subscriptions().await {
if !id.as_str().starts_with(MESSAGE_AUTHOR_SUBSCRIPTION_PREFIX) {
continue;
}
let kept = match existing.as_slice() {
[filter] => missing.iter().position(|chunk| *chunk == filter),
_ => None,
};
match kept {
Some(index) => {
missing.swap_remove(index);
used.insert(id.to_string());
}
None => {
let _ = relay.unsubscribe(&id).await;
}
}
}
if chunks.is_empty() || relay.status() != RelayStatus::Connected {
continue;
}
attempted = attempted.saturating_add(1);
let mut relay_error = None;
let mut next = 0usize;
for chunk in missing {
let id = loop {
let id = format!("{MESSAGE_AUTHOR_SUBSCRIPTION_PREFIX}{next}");
next += 1;
if used.insert(id.clone()) {
break id;
}
};
if let Err(error) = relay
.subscribe_with_id(
SubscriptionId::new(id),
vec![chunk.clone()],
SubscribeOptions::default(),
)
.await
{
relay_error = Some(error.to_string());
}
}
match relay_error {
None => accepted = accepted.saturating_add(1),
Some(error) => last_error = Some(error),
}
}
if chunks.is_empty() || accepted > 0 {
Ok(())
} else if attempted == 0 {
Err("no connected relays".to_string())
} else {
Err(last_error.unwrap_or_else(|| "no relay accepted message subscriptions".to_string()))
}
}
#[cfg(test)]
mod tests;