2023-01-22 17:26:54 -05:00
use crate ::db ::QueryResult ;
use crate ::error ::Result ;
use crate ::event ::{ single_char_tagname , Event } ;
use crate ::nip05 ::{ Nip05Name , VerificationRecord } ;
2023-02-25 16:38:26 -05:00
use crate ::payment ::{ InvoiceInfo , InvoiceStatus } ;
2023-01-22 17:26:54 -05:00
use crate ::repo ::{ now_jitter , NostrRepo } ;
use crate ::subscription ::{ ReqFilter , Subscription } ;
use async_std ::stream ::StreamExt ;
use async_trait ::async_trait ;
use chrono ::{ DateTime , TimeZone , Utc } ;
use sqlx ::postgres ::PgRow ;
2023-02-08 10:55:17 -05:00
use sqlx ::Error ::RowNotFound ;
2023-01-22 17:26:54 -05:00
use sqlx ::{ Error , Execute , FromRow , Postgres , QueryBuilder , Row } ;
use std ::time ::{ Duration , Instant } ;
2023-02-08 10:55:17 -05:00
use crate ::error ;
2023-01-22 17:26:54 -05:00
use crate ::repo ::postgres_migration ::run_migrations ;
use crate ::server ::NostrMetrics ;
2023-02-25 15:49:35 -05:00
use crate ::utils ::{ self , is_hex , is_lower_hex } ;
2023-02-25 16:38:26 -05:00
use nostr ::key ::Keys ;
2023-01-22 17:26:54 -05:00
use tokio ::sync ::mpsc ::Sender ;
use tokio ::sync ::oneshot ::Receiver ;
2023-08-05 12:37:23 -04:00
use tracing ::{ debug , error , info , trace , warn } ;
2023-01-22 17:26:54 -05:00
pub type PostgresPool = sqlx ::pool ::Pool < Postgres > ;
pub struct PostgresRepo {
conn : PostgresPool ,
2023-04-20 07:01:28 -04:00
conn_write : PostgresPool ,
2023-01-22 17:26:54 -05:00
metrics : NostrMetrics ,
}
impl PostgresRepo {
2023-04-20 07:01:28 -04:00
pub fn new ( c : PostgresPool , cw : PostgresPool , m : NostrMetrics ) -> PostgresRepo {
2023-01-22 17:26:54 -05:00
PostgresRepo {
conn : c ,
2023-04-20 07:01:28 -04:00
conn_write : cw ,
2023-01-22 17:26:54 -05:00
metrics : m ,
}
}
2023-02-17 14:25:56 -05:00
}
/// Cleanup expired events on a regular basis
async fn cleanup_expired ( conn : PostgresPool , frequency : Duration ) -> Result < ( ) > {
tokio ::task ::spawn ( async move {
loop {
tokio ::select! {
_ = tokio ::time ::sleep ( frequency ) = > {
let start = Instant ::now ( ) ;
let exp_res = delete_expired ( conn . clone ( ) ) . await ;
match exp_res {
Ok ( exp_count ) = > {
if exp_count > 0 {
info! ( " removed {} expired events in: {:?} " , exp_count , start . elapsed ( ) ) ;
}
} ,
Err ( e ) = > {
warn! ( " could not remove expired events due to error: {:?} " , e ) ;
}
}
}
2023-11-24 07:52:25 -05:00
} ;
2023-02-17 14:25:56 -05:00
}
} ) ;
Ok ( ( ) )
}
/// One-time deletion of all expired events
2023-02-25 15:49:35 -05:00
async fn delete_expired ( conn : PostgresPool ) -> Result < u64 > {
2023-02-17 14:25:56 -05:00
let mut tx = conn . begin ( ) . await ? ;
let update_count = sqlx ::query ( " DELETE FROM \" event \" WHERE expires_at <= $1; " )
. bind ( Utc . timestamp_opt ( utils ::unix_time ( ) as i64 , 0 ) . unwrap ( ) )
. execute ( & mut tx )
2023-02-25 15:49:35 -05:00
. await ?
. rows_affected ( ) ;
2023-02-17 14:25:56 -05:00
tx . commit ( ) . await ? ;
Ok ( update_count )
2023-01-22 17:26:54 -05:00
}
#[ async_trait ]
impl NostrRepo for PostgresRepo {
async fn start ( & self ) -> Result < ( ) > {
2023-02-17 14:25:56 -05:00
// begin a cleanup task for expired events.
2023-04-20 07:01:28 -04:00
cleanup_expired ( self . conn_write . clone ( ) , Duration ::from_secs ( 600 ) ) . await ? ;
2023-02-25 15:49:35 -05:00
Ok ( ( ) )
2023-01-22 17:26:54 -05:00
}
async fn migrate_up ( & self ) -> Result < usize > {
2023-04-20 07:01:28 -04:00
Ok ( run_migrations ( & self . conn_write ) . await ? )
2023-01-22 17:26:54 -05:00
}
async fn write_event ( & self , e : & Event ) -> Result < u64 > {
// start transaction
2023-04-20 07:01:28 -04:00
let mut tx = self . conn_write . begin ( ) . await ? ;
2023-01-22 17:26:54 -05:00
let start = Instant ::now ( ) ;
// get relevant fields from event and convert to blobs.
let id_blob = hex ::decode ( & e . id ) . ok ( ) ;
let pubkey_blob : Option < Vec < u8 > > = hex ::decode ( & e . pubkey ) . ok ( ) ;
let delegator_blob : Option < Vec < u8 > > =
e . delegated_by . as_ref ( ) . and_then ( | d | hex ::decode ( d ) . ok ( ) ) ;
let event_str = serde_json ::to_string ( & e ) . unwrap ( ) ;
2023-01-30 19:02:40 -05:00
// determine if this event would be shadowed by an existing
// replaceable event or parameterized replaceable event.
if e . is_replaceable ( ) {
let repl_count = sqlx ::query (
2023-02-07 16:47:28 -05:00
" SELECT e.id FROM event e WHERE e.pub_key=$1 AND e.kind=$2 AND e.created_at >= $3 LIMIT 1; " )
2023-01-30 19:02:40 -05:00
. bind ( & pubkey_blob )
. bind ( e . kind as i64 )
. bind ( Utc . timestamp_opt ( e . created_at as i64 , 0 ) . unwrap ( ) )
. fetch_optional ( & mut tx )
. await ? ;
if repl_count . is_some ( ) {
return Ok ( 0 ) ;
}
}
if let Some ( d_tag ) = e . distinct_param ( ) {
2023-02-08 10:55:17 -05:00
let repl_count : i64 = if is_lower_hex ( & d_tag ) & & ( d_tag . len ( ) % 2 = = 0 ) {
2023-02-06 07:43:09 -05:00
sqlx ::query_scalar (
2023-01-30 19:02:40 -05:00
" SELECT count(*) AS count FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.pub_key=$1 AND e.kind=$2 AND t.name='d' AND t.value_hex=$3 AND e.created_at >= $4 LIMIT 1; " )
. bind ( hex ::decode ( & e . pubkey ) . ok ( ) )
. bind ( e . kind as i64 )
. bind ( hex ::decode ( d_tag ) . ok ( ) )
. bind ( Utc . timestamp_opt ( e . created_at as i64 , 0 ) . unwrap ( ) )
. fetch_one ( & mut tx )
2023-02-06 07:43:09 -05:00
. await ?
2023-01-30 19:02:40 -05:00
} else {
2023-02-06 07:43:09 -05:00
sqlx ::query_scalar (
2023-01-30 19:02:40 -05:00
" SELECT count(*) AS count FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.pub_key=$1 AND e.kind=$2 AND t.name='d' AND t.value=$3 AND e.created_at >= $4 LIMIT 1; " )
. bind ( hex ::decode ( & e . pubkey ) . ok ( ) )
. bind ( e . kind as i64 )
. bind ( d_tag . as_bytes ( ) )
. bind ( Utc . timestamp_opt ( e . created_at as i64 , 0 ) . unwrap ( ) )
. fetch_one ( & mut tx )
2023-02-06 07:43:09 -05:00
. await ?
} ;
2023-01-30 19:02:40 -05:00
// if any rows were returned, then some newer event with
// the same author/kind/tag value exist, and we can ignore
// this event.
if repl_count > 0 {
2023-02-08 10:55:17 -05:00
return Ok ( 0 ) ;
2023-01-30 19:02:40 -05:00
}
}
2023-01-22 17:26:54 -05:00
// ignore if the event hash is a duplicate.
let mut ins_count = sqlx ::query (
r #" INSERT INTO " event "
2023-02-17 14:25:56 -05:00
( id , pub_key , created_at , expires_at , kind , " content " , delegated_by )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 )
2023-01-22 17:26:54 -05:00
ON CONFLICT ( id ) DO NOTHING " #,
)
2023-11-24 07:52:25 -05:00
. bind ( & id_blob )
. bind ( & pubkey_blob )
. bind ( Utc . timestamp_opt ( e . created_at as i64 , 0 ) . unwrap ( ) )
. bind (
e . expiration ( )
. and_then ( | x | Utc . timestamp_opt ( x as i64 , 0 ) . latest ( ) ) ,
)
. bind ( e . kind as i64 )
. bind ( event_str . into_bytes ( ) )
. bind ( delegator_blob )
. execute ( & mut tx )
. await ?
. rows_affected ( ) ;
2023-02-25 16:38:26 -05:00
2023-01-22 17:26:54 -05:00
if ins_count = = 0 {
// if the event was a duplicate, no need to insert event or
// pubkey references. This will abort the txn.
return Ok ( 0 ) ;
}
// add all tags to the tag table
for tag in e . tags . iter ( ) {
// ensure we have 2 values.
if tag . len ( ) > = 2 {
let tag_name = & tag [ 0 ] ;
let tag_val = & tag [ 1 ] ;
// only single-char tags are searchable
let tag_char_opt = single_char_tagname ( tag_name ) ;
match & tag_char_opt {
Some ( _ ) = > {
// if tag value is lowercase hex;
if is_lower_hex ( tag_val ) & & ( tag_val . len ( ) % 2 = = 0 ) {
2023-02-07 16:47:28 -05:00
sqlx ::query ( " INSERT INTO tag (event_id, \" name \" , value, value_hex) VALUES($1, $2, NULL, $3) \
ON CONFLICT ( event_id , \ " name \" , value, value_hex) DO NOTHING " )
2023-01-22 17:26:54 -05:00
. bind ( & id_blob )
. bind ( tag_name )
. bind ( hex ::decode ( tag_val ) . ok ( ) )
. execute ( & mut tx )
2023-02-25 16:38:26 -05:00
. await
. unwrap ( ) ;
2023-01-22 17:26:54 -05:00
} else {
2023-02-07 16:47:28 -05:00
sqlx ::query ( " INSERT INTO tag (event_id, \" name \" , value, value_hex) VALUES($1, $2, $3, NULL) \
ON CONFLICT ( event_id , \ " name \" , value, value_hex) DO NOTHING " )
2023-01-22 17:26:54 -05:00
. bind ( & id_blob )
. bind ( tag_name )
. bind ( tag_val . as_bytes ( ) )
. execute ( & mut tx )
2023-02-25 16:38:26 -05:00
. await
. unwrap ( ) ;
2023-01-22 17:26:54 -05:00
}
}
None = > { }
}
}
}
2023-01-24 09:04:42 -05:00
if e . is_replaceable ( ) {
let update_count = sqlx ::query ( " DELETE FROM \" event \" WHERE kind=$1 and pub_key = $2 and id not in (select id from \" event \" where kind=$1 and pub_key=$2 order by created_at desc limit 1); " )
2023-01-22 17:26:54 -05:00
. bind ( e . kind as i64 )
. bind ( hex ::decode ( & e . pubkey ) . ok ( ) )
. execute ( & mut tx )
2023-01-24 09:04:42 -05:00
. await ? . rows_affected ( ) ;
2023-01-22 17:26:54 -05:00
if update_count > 0 {
info! (
" hid {} older replaceable kind {} events for author: {:?} " ,
update_count ,
e . kind ,
e . get_author_prefix ( )
) ;
}
}
2023-01-28 18:09:21 -05:00
// parameterized replaceable events
// check for parameterized replaceable events that would be hidden; don't insert these either.
if let Some ( d_tag ) = e . distinct_param ( ) {
2023-02-06 07:43:09 -05:00
let update_count = if is_lower_hex ( & d_tag ) & & ( d_tag . len ( ) % 2 = = 0 ) {
sqlx ::query ( " DELETE FROM event WHERE kind=$1 AND pub_key=$2 AND id IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=$1 AND e.pub_key=$2 AND t.name='d' AND t.value_hex=$3 ORDER BY created_at DESC OFFSET 1); " )
2023-01-28 18:09:21 -05:00
. bind ( e . kind as i64 )
. bind ( hex ::decode ( & e . pubkey ) . ok ( ) )
. bind ( hex ::decode ( d_tag ) . ok ( ) )
. execute ( & mut tx )
2023-02-06 07:43:09 -05:00
. await ? . rows_affected ( )
2023-01-28 18:09:21 -05:00
} else {
2023-02-06 07:43:09 -05:00
sqlx ::query ( " DELETE FROM event WHERE kind=$1 AND pub_key=$2 AND id IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=$1 AND e.pub_key=$2 AND t.name='d' AND t.value=$3 ORDER BY created_at DESC OFFSET 1); " )
2023-01-28 18:09:21 -05:00
. bind ( e . kind as i64 )
. bind ( hex ::decode ( & e . pubkey ) . ok ( ) )
. bind ( d_tag . as_bytes ( ) )
. execute ( & mut tx )
2023-02-06 07:43:09 -05:00
. await ? . rows_affected ( )
} ;
2023-01-28 18:09:21 -05:00
if update_count > 0 {
info! (
" removed {} older parameterized replaceable kind {} events for author: {:?} " ,
update_count ,
e . kind ,
e . get_author_prefix ( )
) ;
}
}
2023-01-22 17:26:54 -05:00
// if this event is a deletion, hide the referenced events from the same author.
if e . kind = = 5 {
let event_candidates = e . tag_values_by_name ( " e " ) ;
let pub_keys : Vec < Vec < u8 > > = event_candidates
. iter ( )
. filter ( | x | is_hex ( x ) & & x . len ( ) = = 64 )
. filter_map ( | x | hex ::decode ( x ) . ok ( ) )
. collect ( ) ;
let mut builder = QueryBuilder ::new (
" UPDATE \" event \" SET hidden = 1::bit(1) WHERE kind != 5 AND pub_key = " ,
) ;
builder . push_bind ( hex ::decode ( & e . pubkey ) . ok ( ) ) ;
builder . push ( " AND id IN ( " ) ;
let mut sep = builder . separated ( " , " ) ;
for pk in pub_keys {
sep . push_bind ( pk ) ;
}
sep . push_unseparated ( " ) " ) ;
let update_count = builder . build ( ) . execute ( & mut tx ) . await ? . rows_affected ( ) ;
info! (
" hid {} deleted events for author {:?} " ,
update_count ,
e . get_author_prefix ( )
) ;
} else {
// check if a deletion has already been recorded for this event.
// Only relevant for non-deletion events
let del_count = sqlx ::query (
" SELECT e.id FROM \" event \" e \
LEFT JOIN tag t ON e . id = t . event_id \
WHERE e . pub_key = $ 1 AND t . \ " name \" = 'e' AND e.kind = 5 AND t.value = $2 LIMIT 1 " ,
)
2023-11-24 07:52:25 -05:00
. bind ( & pubkey_blob )
. bind ( & id_blob )
. fetch_optional ( & mut tx )
. await ? ;
2023-01-22 17:26:54 -05:00
// check if a the query returned a result, meaning we should
// hid the current event
if del_count . is_some ( ) {
// a deletion already existed, mark original event as hidden.
info! (
" hid event: {:?} due to existing deletion by author: {:?} " ,
e . get_event_id_prefix ( ) ,
e . get_author_prefix ( )
) ;
sqlx ::query ( " UPDATE \" event \" SET hidden = 1::bit(1) WHERE id = $1 " )
. bind ( & id_blob )
. execute ( & mut tx )
. await ? ;
// event was deleted, so let caller know nothing new
// arrived, preventing this from being sent to active
// subscriptions
ins_count = 0 ;
}
}
tx . commit ( ) . await ? ;
self . metrics
. write_events
. observe ( start . elapsed ( ) . as_secs_f64 ( ) ) ;
Ok ( ins_count )
}
async fn query_subscription (
& self ,
sub : Subscription ,
client_id : String ,
query_tx : Sender < QueryResult > ,
mut abandon_query_rx : Receiver < ( ) > ,
) -> Result < ( ) > {
let start = Instant ::now ( ) ;
let mut row_count : usize = 0 ;
2023-01-28 17:05:58 -05:00
let metrics = & self . metrics ;
2023-01-22 17:26:54 -05:00
for filter in sub . filters . iter ( ) {
let start = Instant ::now ( ) ;
// generate SQL query
let q_filter = query_from_filter ( filter ) ;
if q_filter . is_none ( ) {
debug! ( " Failed to generate query! " ) ;
continue ;
}
debug! ( " SQL generated in {:?} " , start . elapsed ( ) ) ;
// cutoff for displaying slow queries
let slow_cutoff = Duration ::from_millis ( 2000 ) ;
// any client that doesn't cause us to generate new rows in 5
// seconds gets dropped.
let abort_cutoff = Duration ::from_secs ( 5 ) ;
let start = Instant ::now ( ) ;
let mut slow_first_event ;
let mut last_successful_send = Instant ::now ( ) ;
// execute the query. Don't cache, since queries vary so much.
let mut q_filter = q_filter . unwrap ( ) ;
let q_build = q_filter . build ( ) ;
let sql = q_build . sql ( ) ;
let mut results = q_build . fetch ( & self . conn ) ;
let mut first_result = true ;
while let Some ( row ) = results . next ( ) . await {
if let Err ( e ) = row {
error! ( " Query failed: {} {} {:?} " , e , sql , filter ) ;
break ;
}
let first_event_elapsed = start . elapsed ( ) ;
slow_first_event = first_event_elapsed > = slow_cutoff ;
if first_result {
debug! (
" first result in {:?} (cid: {}, sub: {:?}) " ,
first_event_elapsed , client_id , sub . id
) ;
first_result = false ;
}
// logging for slow queries; show sub and SQL.
// to reduce logging; only show 1/16th of clients (leading 0)
if slow_first_event & & client_id . starts_with ( " 00 " ) {
debug! (
" query req (slow): {:?} (cid: {}, sub: {:?}) " ,
& sub , client_id , sub . id
) ;
} else {
trace! (
" query req: {:?} (cid: {}, sub: {:?}) " ,
& sub ,
client_id ,
sub . id
) ;
}
// check if this is still active; every 100 rows
if row_count % 100 = = 0 & & abandon_query_rx . try_recv ( ) . is_ok ( ) {
2023-02-08 10:55:17 -05:00
debug! (
" query cancelled by client (cid: {}, sub: {:?}) " ,
client_id , sub . id
) ;
2023-01-22 17:26:54 -05:00
return Ok ( ( ) ) ;
}
row_count + = 1 ;
let event_json : Vec < u8 > = row . unwrap ( ) . get ( 0 ) ;
loop {
if query_tx . capacity ( ) ! = 0 {
// we have capacity to add another item
break ;
} else {
// the queue is full
trace! ( " db reader thread is stalled " ) ;
if last_successful_send + abort_cutoff < Instant ::now ( ) {
// the queue has been full for too long, abort
info! ( " aborting database query due to slow client " ) ;
2023-02-08 10:55:17 -05:00
metrics
. query_aborts
. with_label_values ( & [ " slowclient " ] )
. inc ( ) ;
2023-01-22 17:26:54 -05:00
return Ok ( ( ) ) ;
}
// give the queue a chance to clear before trying again
async_std ::task ::sleep ( Duration ::from_millis ( 100 ) ) . await ;
}
}
// TODO: we could use try_send, but we'd have to juggle
// getting the query result back as part of the error
// result.
query_tx
. send ( QueryResult {
sub_id : sub . get_id ( ) ,
event : String ::from_utf8 ( event_json ) . unwrap ( ) ,
} )
. await
. ok ( ) ;
last_successful_send = Instant ::now ( ) ;
}
}
query_tx
. send ( QueryResult {
sub_id : sub . get_id ( ) ,
event : " EOSE " . to_string ( ) ,
} )
. await
. ok ( ) ;
self . metrics
. query_sub
. observe ( start . elapsed ( ) . as_secs_f64 ( ) ) ;
debug! (
" query completed in {:?} (cid: {}, sub: {:?}, db_time: {:?}, rows: {}) " ,
start . elapsed ( ) ,
client_id ,
sub . id ,
start . elapsed ( ) ,
row_count
) ;
Ok ( ( ) )
}
async fn optimize_db ( & self ) -> Result < ( ) > {
// Not implemented
Ok ( ( ) )
}
async fn create_verification_record ( & self , event_id : & str , name : & str ) -> Result < ( ) > {
2023-04-20 07:01:28 -04:00
let mut tx = self . conn_write . begin ( ) . await ? ;
2023-01-22 17:26:54 -05:00
sqlx ::query ( " DELETE FROM user_verification WHERE \" name \" = $1 " )
. bind ( name )
. execute ( & mut tx )
. await ? ;
sqlx ::query ( " INSERT INTO user_verification (event_id, \" name \" , verified_at) VALUES ($1, $2, now()) " )
. bind ( hex ::decode ( event_id ) . ok ( ) )
. bind ( name )
. execute ( & mut tx )
. await ? ;
tx . commit ( ) . await ? ;
info! ( " saved new verification record for ({:?}) " , name ) ;
Ok ( ( ) )
}
async fn update_verification_timestamp ( & self , id : u64 ) -> Result < ( ) > {
// add some jitter to the verification to prevent everything from stacking up together.
let verify_time = now_jitter ( 600 ) ;
// update verification time and reset any failure count
2023-02-08 10:55:17 -05:00
sqlx ::query ( " UPDATE user_verification SET verified_at = $1, fail_count = 0 WHERE id = $2 " )
2023-01-22 17:26:54 -05:00
. bind ( Utc . timestamp_opt ( verify_time as i64 , 0 ) . unwrap ( ) )
. bind ( id as i64 )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-01-22 17:26:54 -05:00
. await ? ;
info! ( " verification updated for {} " , id ) ;
Ok ( ( ) )
}
async fn fail_verification ( & self , id : u64 ) -> Result < ( ) > {
sqlx ::query ( " UPDATE user_verification SET failed_at = now(), fail_count = fail_count + 1 WHERE id = $1 " )
. bind ( id as i64 )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-01-22 17:26:54 -05:00
. await ? ;
Ok ( ( ) )
}
async fn delete_verification ( & self , id : u64 ) -> Result < ( ) > {
sqlx ::query ( " DELETE FROM user_verification WHERE id = $1 " )
. bind ( id as i64 )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-01-22 17:26:54 -05:00
. await ? ;
Ok ( ( ) )
}
async fn get_latest_user_verification ( & self , pub_key : & str ) -> Result < VerificationRecord > {
let query = r #" SELECT
v . id ,
v . " name " ,
e . id as event_id ,
e . pub_key ,
e . created_at ,
v . verified_at ,
v . failed_at ,
v . fail_count
FROM user_verification v
LEFT JOIN " event " e ON e . id = v . event_id
WHERE e . pub_key = $ 1
ORDER BY e . created_at DESC , v . verified_at DESC , v . failed_at DESC
LIMIT 1 " #;
sqlx ::query_as ::< _ , VerificationRecord > ( query )
. bind ( hex ::decode ( pub_key ) . ok ( ) )
. fetch_optional ( & self . conn )
. await ?
. ok_or ( error ::Error ::SqlxError ( RowNotFound ) )
}
async fn get_oldest_user_verification ( & self , before : u64 ) -> Result < VerificationRecord > {
let query = r #" SELECT
v . id ,
v . " name " ,
e . id as event_id ,
e . pub_key ,
e . created_at ,
v . verified_at ,
v . failed_at ,
v . fail_count
FROM user_verification v
LEFT JOIN " event " e ON e . id = v . event_id
WHERE ( v . verified_at < $ 1 OR v . verified_at IS NULL )
AND ( v . failed_at < $ 1 OR v . failed_at IS NULL )
ORDER BY v . verified_at ASC , v . failed_at ASC
LIMIT 1 " #;
sqlx ::query_as ::< _ , VerificationRecord > ( query )
. bind ( Utc . timestamp_opt ( before as i64 , 0 ) . unwrap ( ) )
. fetch_optional ( & self . conn )
. await ?
. ok_or ( error ::Error ::SqlxError ( RowNotFound ) )
}
2023-02-25 16:38:26 -05:00
async fn create_account ( & self , pub_key : & Keys ) -> Result < bool > {
let pub_key = pub_key . public_key ( ) . to_string ( ) ;
2023-04-20 07:01:28 -04:00
let mut tx = self . conn_write . begin ( ) . await ? ;
2023-02-25 16:38:26 -05:00
let result = sqlx ::query ( " INSERT INTO account (pubkey, balance) VALUES ($1, 0); " )
. bind ( pub_key )
. execute ( & mut tx )
. await ;
let success = match result {
Ok ( res ) = > {
tx . commit ( ) . await ? ;
res . rows_affected ( ) = = 1
}
Err ( _err ) = > false ,
} ;
Ok ( success )
}
/// Admit account
async fn admit_account ( & self , pub_key : & Keys , admission_cost : u64 ) -> Result < ( ) > {
let pub_key = pub_key . public_key ( ) . to_string ( ) ;
sqlx ::query (
" UPDATE account SET is_admitted = TRUE, balance = balance - $1 WHERE pubkey = $2 " ,
)
2023-11-24 07:52:25 -05:00
. bind ( admission_cost as i64 )
. bind ( pub_key )
. execute ( & self . conn_write )
. await ? ;
2023-02-25 16:38:26 -05:00
Ok ( ( ) )
}
/// Gets if the account is admitted and balance
async fn get_account_balance ( & self , pub_key : & Keys ) -> Result < ( bool , u64 ) > {
let pub_key = pub_key . public_key ( ) . to_string ( ) ;
let query = r #" SELECT
is_admitted ,
balance
FROM account
WHERE pubkey = $ 1
LIMIT 1 " #;
let result = sqlx ::query_as ::< _ , ( bool , i64 ) > ( query )
. bind ( pub_key )
2023-04-20 07:01:28 -04:00
. fetch_optional ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ?
. ok_or ( error ::Error ::SqlxError ( RowNotFound ) ) ? ;
Ok ( ( result . 0 , result . 1 as u64 ) )
}
/// Update account balance
async fn update_account_balance (
& self ,
pub_key : & Keys ,
positive : bool ,
new_balance : u64 ,
) -> Result < ( ) > {
let pub_key = pub_key . public_key ( ) . to_string ( ) ;
match positive {
true = > {
sqlx ::query ( " UPDATE account SET balance = balance + $1 WHERE pubkey = $2 " )
. bind ( new_balance as i64 )
. bind ( pub_key )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ?
}
false = > {
sqlx ::query ( " UPDATE account SET balance = balance - $1 WHERE pubkey = $2 " )
. bind ( new_balance as i64 )
. bind ( pub_key )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ?
}
} ;
Ok ( ( ) )
}
/// Create invoice record
async fn create_invoice_record ( & self , pub_key : & Keys , invoice_info : InvoiceInfo ) -> Result < ( ) > {
let pub_key = pub_key . public_key ( ) . to_string ( ) ;
2023-04-20 07:01:28 -04:00
let mut tx = self . conn_write . begin ( ) . await ? ;
2023-02-25 16:38:26 -05:00
sqlx ::query (
" INSERT INTO invoice (pubkey, payment_hash, amount, status, description, created_at, invoice) VALUES ($1, $2, $3, $4, $5, now(), $6) " ,
)
2023-10-17 11:07:56 -04:00
. bind ( pub_key )
. bind ( invoice_info . payment_hash )
. bind ( invoice_info . amount as i64 )
. bind ( invoice_info . status )
. bind ( invoice_info . memo )
. bind ( invoice_info . bolt11 )
. execute ( & mut tx )
. await . unwrap ( ) ;
2023-02-25 16:38:26 -05:00
debug! ( " Invoice added " ) ;
tx . commit ( ) . await ? ;
Ok ( ( ) )
}
/// Update invoice record
async fn update_invoice ( & self , payment_hash : & str , status : InvoiceStatus ) -> Result < String > {
debug! ( " Payment Hash: {} " , payment_hash ) ;
let query = " SELECT pubkey, status, amount FROM invoice WHERE payment_hash=$1; " ;
let ( pubkey , prev_invoice_status , amount ) =
sqlx ::query_as ::< _ , ( String , InvoiceStatus , i64 ) > ( query )
. bind ( payment_hash )
2023-04-20 07:01:28 -04:00
. fetch_optional ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ?
. ok_or ( error ::Error ::SqlxError ( RowNotFound ) ) ? ;
// If the invoice is paid update the confirmed at timestamp
let query = if status . eq ( & InvoiceStatus ::Paid ) {
" UPDATE invoice SET status=$1, confirmed_at = now() WHERE payment_hash=$2; "
} else {
" UPDATE invoice SET status=$1 WHERE payment_hash=$2; "
} ;
sqlx ::query ( query )
. bind ( & status )
. bind ( payment_hash )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ? ;
if prev_invoice_status . eq ( & InvoiceStatus ::Unpaid ) & & status . eq ( & InvoiceStatus ::Paid ) {
sqlx ::query ( " UPDATE account SET balance = balance + $1 WHERE pubkey = $2 " )
. bind ( amount )
. bind ( & pubkey )
2023-04-20 07:01:28 -04:00
. execute ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await ? ;
}
Ok ( pubkey )
}
/// Get the most recent invoice for a given pubkey
/// invoice must be unpaid and not expired
async fn get_unpaid_invoice ( & self , pubkey : & Keys ) -> Result < Option < InvoiceInfo > > {
let query = r #"
SELECT amount , payment_hash , description , invoice
FROM invoice
WHERE pubkey = $ 1
ORDER BY created_at DESC
LIMIT 1 ;
" #;
match sqlx ::query_as ::< _ , ( i64 , String , String , String ) > ( query )
. bind ( pubkey . public_key ( ) . to_string ( ) )
2023-04-20 07:01:28 -04:00
. fetch_optional ( & self . conn_write )
2023-02-25 16:38:26 -05:00
. await
. unwrap ( )
{
Some ( ( amount , payment_hash , description , invoice ) ) = > Ok ( Some ( InvoiceInfo {
pubkey : pubkey . public_key ( ) . to_string ( ) ,
payment_hash ,
bolt11 : invoice ,
amount : amount as u64 ,
status : InvoiceStatus ::Unpaid ,
memo : description ,
confirmed_at : None ,
} ) ) ,
None = > Ok ( None ) ,
}
}
2023-01-22 17:26:54 -05:00
}
/// Create a dynamic SQL query and params from a subscription filter.
fn query_from_filter ( f : & ReqFilter ) -> Option < QueryBuilder < Postgres > > {
// if the filter is malformed, don't return anything.
if f . force_no_match {
return None ;
}
let mut query = QueryBuilder ::new ( " SELECT e. \" content \" , e.created_at FROM \" event \" e WHERE " ) ;
2023-11-24 07:52:25 -05:00
2023-02-17 22:04:30 -05:00
// This tracks whether we need to push a prefix AND before adding another clause
2023-01-22 17:26:54 -05:00
let mut push_and = false ;
// Query for "authors", allowing prefix matches
if let Some ( auth_vec ) = & f . authors {
// filter out non-hex values
let auth_vec : Vec < & String > = auth_vec . iter ( ) . filter ( | a | is_hex ( a ) ) . collect ( ) ;
2023-11-22 19:15:18 -05:00
if auth_vec . is_empty ( ) {
return None ;
}
2023-11-24 07:52:25 -05:00
query . push ( " (e.pub_key in ( " ) ;
let mut pk_sep = query . separated ( " , " ) ;
for pk in auth_vec . iter ( ) {
pk_sep . push_bind ( hex ::decode ( pk ) . ok ( ) ) ;
2023-01-22 17:26:54 -05:00
}
2023-11-24 07:52:25 -05:00
query . push ( " ) OR e.delegated_by in ( " ) ;
let mut pk_delegated_sep = query . separated ( " , " ) ;
for pk in auth_vec . iter ( ) {
pk_delegated_sep . push_bind ( hex ::decode ( pk ) . ok ( ) ) ;
}
push_and = true ;
query . push ( " )) " ) ;
2023-01-22 17:26:54 -05:00
}
// Query for Kind
if let Some ( ks ) = & f . kinds {
2023-11-22 19:15:18 -05:00
if ks . is_empty ( ) {
return None ;
}
if push_and {
query . push ( " AND " ) ;
}
push_and = true ;
2023-01-22 17:26:54 -05:00
2023-11-22 19:15:18 -05:00
query . push ( " e.kind in ( " ) ;
let mut list_query = query . separated ( " , " ) ;
for k in ks . iter ( ) {
list_query . push_bind ( * k as i64 ) ;
2023-01-22 17:26:54 -05:00
}
2023-11-22 19:15:18 -05:00
query . push ( " ) " ) ;
2023-01-22 17:26:54 -05:00
}
2023-11-24 07:52:25 -05:00
// Query for event,
2023-01-22 17:26:54 -05:00
if let Some ( id_vec ) = & f . ids {
// filter out non-hex values
let id_vec : Vec < & String > = id_vec . iter ( ) . filter ( | a | is_hex ( a ) ) . collect ( ) ;
2023-11-22 19:15:18 -05:00
if id_vec . is_empty ( ) {
return None ;
}
if push_and {
query . push ( " AND ( " ) ;
} else {
query . push ( " ( " ) ;
}
push_and = true ;
2023-01-22 17:26:54 -05:00
2023-11-24 07:52:25 -05:00
query . push ( " id in ( " ) ;
let mut sep = query . separated ( " , " ) ;
for id in id_vec . iter ( ) {
sep . push_bind ( hex ::decode ( id ) . ok ( ) ) ;
2023-01-22 17:26:54 -05:00
}
2023-11-24 07:52:25 -05:00
query . push ( " )) " ) ;
2023-01-22 17:26:54 -05:00
}
// Query for tags
if let Some ( map ) = & f . tags {
if ! map . is_empty ( ) {
if push_and {
query . push ( " AND " ) ;
}
push_and = true ;
2023-11-22 18:54:22 -05:00
let mut push_or = false ;
query . push ( " e.id IN (SELECT ee.id FROM \" event \" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and " ) ;
2023-01-22 17:26:54 -05:00
for ( key , val ) in map . iter ( ) {
2023-11-22 19:15:18 -05:00
if val . is_empty ( ) {
return None ;
}
2023-11-22 18:54:22 -05:00
if push_or {
query . push ( " OR " ) ;
}
2023-11-24 07:52:25 -05:00
query
. push ( " (t. \" name \" = " )
2023-01-22 17:26:54 -05:00
. push_bind ( key . to_string ( ) )
2023-10-17 11:07:56 -04:00
. push ( " AND ( " ) ;
2024-03-30 17:22:37 -04:00
let has_plain_values = val . iter ( ) . any ( | v | ( v . len ( ) % 2 ! = 0 | | ! is_lower_hex ( v ) ) ) ;
2023-10-17 11:07:56 -04:00
let has_hex_values = val . iter ( ) . any ( | v | v . len ( ) % 2 = = 0 & & is_lower_hex ( v ) ) ;
if has_plain_values {
query . push ( " value in ( " ) ;
// plain value match first
let mut tag_query = query . separated ( " , " ) ;
2024-03-30 17:22:37 -04:00
for v in val . iter ( ) . filter ( | v | v . len ( ) % 2 ! = 0 | | ! is_lower_hex ( v ) ) {
2023-01-22 17:26:54 -05:00
tag_query . push_bind ( v . as_bytes ( ) ) ;
2023-10-17 11:07:56 -04:00
}
}
if has_plain_values & & has_hex_values {
query . push ( " ) OR " ) ;
}
if has_hex_values {
query . push ( " value_hex in ( " ) ;
// plain value match first
let mut tag_query = query . separated ( " , " ) ;
2023-11-24 07:52:25 -05:00
for v in val . iter ( ) . filter ( | v | v . len ( ) % 2 = = 0 & & is_lower_hex ( v ) ) {
2023-01-22 17:26:54 -05:00
tag_query . push_bind ( hex ::decode ( v ) . ok ( ) ) ;
}
}
2023-10-17 11:07:56 -04:00
2023-11-22 18:54:22 -05:00
query . push ( " ))) " ) ;
push_or = true ;
2023-01-22 17:26:54 -05:00
}
2023-11-22 18:54:22 -05:00
query . push ( " ) " ) ;
2023-01-22 17:26:54 -05:00
}
}
// Query for timestamp
if f . since . is_some ( ) {
if push_and {
query . push ( " AND " ) ;
}
push_and = true ;
query
2023-07-14 21:57:31 -04:00
. push ( " e.created_at >= " )
2023-01-22 17:26:54 -05:00
. push_bind ( Utc . timestamp_opt ( f . since . unwrap ( ) as i64 , 0 ) . unwrap ( ) ) ;
}
// Query for timestamp
if f . until . is_some ( ) {
if push_and {
query . push ( " AND " ) ;
}
push_and = true ;
query
2023-07-14 21:57:31 -04:00
. push ( " e.created_at <= " )
2023-01-22 17:26:54 -05:00
. push_bind ( Utc . timestamp_opt ( f . until . unwrap ( ) as i64 , 0 ) . unwrap ( ) ) ;
}
// never display hidden events
if push_and {
query . push ( " AND e.hidden != 1::bit(1) " ) ;
} else {
query . push ( " e.hidden != 1::bit(1) " ) ;
}
2023-02-17 14:25:56 -05:00
// never display expired events
2023-10-17 11:07:56 -04:00
query . push ( " AND (e.expires_at IS NULL OR e.expires_at > now()) " ) ;
2023-02-17 14:25:56 -05:00
2023-01-22 17:26:54 -05:00
// Apply per-filter limit to this query.
// The use of a LIMIT implies a DESC order, to capture only the most recent events.
if let Some ( lim ) = f . limit {
query . push ( " ORDER BY e.created_at DESC LIMIT " ) ;
query . push ( lim . min ( 1000 ) ) ;
} else {
query . push ( " ORDER BY e.created_at ASC LIMIT " ) ;
query . push ( 1000 ) ;
}
Some ( query )
}
impl FromRow < '_ , PgRow > for VerificationRecord {
fn from_row ( row : & '_ PgRow ) -> std ::result ::Result < Self , Error > {
2023-02-08 10:55:17 -05:00
let name = Nip05Name ::try_from ( row . get ::< '_ , & str , & str > ( " name " ) ) . or ( Err ( RowNotFound ) ) ? ;
2023-01-22 17:26:54 -05:00
Ok ( VerificationRecord {
rowid : row . get ::< '_ , i64 , & str > ( " id " ) as u64 ,
name ,
address : hex ::encode ( row . get ::< '_ , Vec < u8 > , & str > ( " pub_key " ) ) ,
event : hex ::encode ( row . get ::< '_ , Vec < u8 > , & str > ( " event_id " ) ) ,
event_created : row . get ::< '_ , DateTime < Utc > , & str > ( " created_at " ) . timestamp ( ) as u64 ,
2023-03-02 16:16:21 -05:00
last_success : match row . try_get ::< '_ , DateTime < Utc > , & str > ( " verified_at " ) {
Ok ( x ) = > Some ( x . timestamp ( ) as u64 ) ,
_ = > None ,
} ,
2023-01-22 17:26:54 -05:00
last_failure : match row . try_get ::< '_ , DateTime < Utc > , & str > ( " failed_at " ) {
Ok ( x ) = > Some ( x . timestamp ( ) as u64 ) ,
_ = > None ,
} ,
failure_count : row . get ::< '_ , i32 , & str > ( " fail_count " ) as u64 ,
} )
}
}
2023-10-17 11:07:56 -04:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2023-11-24 07:52:25 -05:00
use std ::collections ::{ HashMap , HashSet } ;
2023-10-17 11:07:56 -04:00
#[ test ]
fn test_query_gen_tag_value_hex ( ) {
let filter = ReqFilter {
ids : None ,
kinds : Some ( vec! [ 1000 ] ) ,
since : None ,
until : None ,
2023-11-24 07:52:25 -05:00
authors : Some ( vec! [
" 84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864 " . to_owned ( ) ,
] ) ,
2023-10-17 11:07:56 -04:00
limit : None ,
2023-11-24 07:52:25 -05:00
tags : Some ( HashMap ::from ( [ (
'p' ,
HashSet ::from ( [
" 63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed " . to_owned ( ) ,
] ) ,
) ] ) ) ,
2023-10-17 11:07:56 -04:00
force_no_match : false ,
} ;
let q = query_from_filter ( & filter ) . unwrap ( ) ;
assert_eq! ( q . sql ( ) , " SELECT e. \" content \" , e.created_at FROM \" event \" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \" event \" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t. \" name \" = $4 AND (value_hex in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000 " )
}
#[ test ]
fn test_query_gen_tag_value ( ) {
let filter = ReqFilter {
ids : None ,
kinds : Some ( vec! [ 1000 ] ) ,
since : None ,
until : None ,
2023-11-24 07:52:25 -05:00
authors : Some ( vec! [
" 84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864 " . to_owned ( ) ,
] ) ,
2023-10-17 11:07:56 -04:00
limit : None ,
2023-11-24 07:52:25 -05:00
tags : Some ( HashMap ::from ( [ ( 'd' , HashSet ::from ( [ " test " . to_owned ( ) ] ) ) ] ) ) ,
2023-10-17 11:07:56 -04:00
force_no_match : false ,
} ;
let q = query_from_filter ( & filter ) . unwrap ( ) ;
assert_eq! ( q . sql ( ) , " SELECT e. \" content \" , e.created_at FROM \" event \" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \" event \" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t. \" name \" = $4 AND (value in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000 " )
}
#[ test ]
fn test_query_gen_tag_value_and_value_hex ( ) {
let filter = ReqFilter {
ids : None ,
kinds : Some ( vec! [ 1000 ] ) ,
since : None ,
until : None ,
2023-11-24 07:52:25 -05:00
authors : Some ( vec! [
" 84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864 " . to_owned ( ) ,
] ) ,
2023-10-17 11:07:56 -04:00
limit : None ,
2023-11-24 07:52:25 -05:00
tags : Some ( HashMap ::from ( [ (
'd' ,
HashSet ::from ( [
" test " . to_owned ( ) ,
" 63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed " . to_owned ( ) ,
] ) ,
) ] ) ) ,
2023-10-17 11:07:56 -04:00
force_no_match : false ,
} ;
let q = query_from_filter ( & filter ) . unwrap ( ) ;
assert_eq! ( q . sql ( ) , " SELECT e. \" content \" , e.created_at FROM \" event \" e WHERE (e.pub_key in ($1) OR e.delegated_by in ($2)) AND e.kind in ($3) AND e.id IN (SELECT ee.id FROM \" event \" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t. \" name \" = $4 AND (value in ($5) OR value_hex in ($6)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000 " )
}
2023-11-22 18:54:22 -05:00
#[ test ]
fn test_query_multiple_tags ( ) {
let filter = ReqFilter {
ids : None ,
kinds : Some ( vec! [ 30_001 ] ) ,
since : None ,
until : None ,
authors : None ,
limit : None ,
tags : Some ( HashMap ::from ( [
( 'd' , HashSet ::from ( [ " follow " . to_owned ( ) ] ) ) ,
2023-11-24 07:52:25 -05:00
( 't' , HashSet ::from ( [ " siamstr " . to_owned ( ) ] ) ) ,
2023-11-22 18:54:22 -05:00
] ) ) ,
force_no_match : false ,
} ;
let q = query_from_filter ( & filter ) . unwrap ( ) ;
assert_eq! ( q . sql ( ) , " SELECT e. \" content \" , e.created_at FROM \" event \" e WHERE e.kind in ($1) AND e.id IN (SELECT ee.id FROM \" event \" ee LEFT JOIN tag t on ee.id = t.event_id WHERE ee.hidden != 1::bit(1) and (t. \" name \" = $2 AND (value in ($3))) OR (t. \" name \" = $4 AND (value in ($5)))) AND e.hidden != 1::bit(1) AND (e.expires_at IS NULL OR e.expires_at > now()) ORDER BY e.created_at ASC LIMIT 1000 " )
}
2023-11-22 19:15:18 -05:00
#[ test ]
fn test_query_empty_tags ( ) {
let filter = ReqFilter {
ids : None ,
kinds : Some ( vec! [ 1 , 6 , 16 , 30023 , 1063 , 6969 ] ) ,
since : Some ( 1700697846 ) ,
until : None ,
authors : None ,
limit : None ,
2023-11-24 07:52:25 -05:00
tags : Some ( HashMap ::from ( [ ( 'a' , HashSet ::new ( ) ) ] ) ) ,
2023-11-22 19:15:18 -05:00
force_no_match : false ,
} ;
assert! ( query_from_filter ( & filter ) . is_none ( ) ) ;
}
2023-11-24 07:52:25 -05:00
}