25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 1 | use anyhow::{Context as _, Result}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 2 | use nostr::filter::Filter; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 3 | use rusqlite::types::Value; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 4 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 5 | const DEFAULT_LIMIT: usize = 10_000; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 6 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 7 | const SELECT_EVENTS: &str = " |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 8 | SELECT e.id, e.pubkey, e.created_at, e.kind, e.content, e.tags, e.sig |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 9 | FROM events AS e |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 10 | WHERE 1 = 1 |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 11 | "; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 12 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 13 | const TAG_VALUES: &str = " |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 14 | AND EXISTS ( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 15 | SELECT 1 |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 16 | FROM event_tags AS tag |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 17 | WHERE tag.event_id = e.id |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 18 | AND tag.tag_name = ? |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 19 | AND tag.tag_value IN ( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 20 | "; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 21 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 22 | pub struct EventQuery { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 23 | pub(crate) sql: String, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 24 | pub(crate) parameters: Vec<Value>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 25 | pub(crate) limit: usize, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 26 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 27 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 28 | impl EventQuery { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 29 | pub(crate) fn new(filter: &Filter) -> Result<Self> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 30 | if filter.search.is_some() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 31 | anyhow::bail!("search is not supported"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 32 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 33 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 34 | let mut query = Self { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 35 | sql: SELECT_EVENTS.to_owned(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 36 | parameters: Vec::new(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 37 | limit: filter.limit.unwrap_or(DEFAULT_LIMIT), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 38 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 39 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 40 | if let Some(ids) = filter.ids.as_ref() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 41 | query.add_values( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 42 | "e.id", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 43 | ids.iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 44 | .map(|id| Value::Blob(id.as_bytes().to_vec())) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 45 | .collect(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 46 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 47 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 48 | if let Some(authors) = filter.authors.as_ref() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 49 | query.add_values( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 50 | "e.pubkey", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 51 | authors |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 52 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 53 | .map(|author| Value::Blob(author.as_bytes().to_vec())) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 54 | .collect(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 55 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 56 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 57 | if let Some(kinds) = filter.kinds.as_ref() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 58 | query.add_values( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 59 | "e.kind", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 60 | kinds |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 61 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 62 | .map(|kind| Value::Integer(i64::from(kind.as_u16()))) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 63 | .collect(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 64 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 65 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 66 | if let Some(since) = filter.since { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 67 | query.add_condition( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 68 | "e.created_at >= ?", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 69 | Value::Integer(timestamp(since.as_secs())?), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 70 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 71 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 72 | if let Some(until) = filter.until { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 73 | query.add_condition( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 74 | "e.created_at <= ?", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 75 | Value::Integer(timestamp(until.as_secs())?), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 76 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 77 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 78 | for (tag, values) in &filter.generic_tags { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 79 | if values.is_empty() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 80 | query.exclude_all(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 81 | break; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 82 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 83 | query.sql.push_str(TAG_VALUES); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 84 | query.parameters.push(Value::Text(tag.to_string())); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 85 | query.add_placeholders(values.len()); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 86 | query.sql.push_str("))"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 87 | query |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 88 | .parameters |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 89 | .extend(values.iter().cloned().map(Value::Text)); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 90 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 91 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 92 | query.sql.push_str(" ORDER BY e.created_at DESC, e.id"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 93 | Ok(query) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 94 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 95 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 96 | fn add_values(&mut self, column: &str, values: Vec<Value>) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 97 | if values.is_empty() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 98 | self.exclude_all(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 99 | return; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 100 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 101 | self.sql.push_str(" AND "); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 102 | self.sql.push_str(column); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 103 | self.sql.push_str(" IN ("); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 104 | self.add_placeholders(values.len()); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 105 | self.sql.push(')'); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 106 | self.parameters.extend(values); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 107 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 108 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 109 | fn add_condition(&mut self, condition: &str, value: Value) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 110 | self.sql.push_str(" AND "); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 111 | self.sql.push_str(condition); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 112 | self.parameters.push(value); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 113 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 114 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 115 | fn add_placeholders(&mut self, count: usize) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 116 | for index in 0..count { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 117 | if index > 0 { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 118 | self.sql.push_str(", "); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 119 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 120 | self.sql.push('?'); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 121 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 122 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 123 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 124 | fn exclude_all(&mut self) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 125 | self.sql.push_str(" AND 0"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 126 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 127 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 128 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 129 | fn timestamp(value: u64) -> Result<i64> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 130 | value.try_into().context("timestamp exceeds sqlite range") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 131 | } |