mirror of
https://github.com/nostrlabs-io/notepush.git
synced 2025-06-15 11:28:23 +00:00
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
edition = "2018"
|
11
shell.nix
11
shell.nix
@ -1,10 +1,11 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = with pkgs; [
|
||||||
pkgs.cargo
|
cargo
|
||||||
pkgs.openssl
|
rustfmt
|
||||||
pkgs.pkg-config
|
openssl
|
||||||
pkgs.websocat
|
pkg-config
|
||||||
|
websocat
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ impl APIHandler {
|
|||||||
base_url,
|
base_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - HTTP handling
|
// MARK: - HTTP handling
|
||||||
|
|
||||||
pub async fn handle_http_request(
|
pub async fn handle_http_request(
|
||||||
@ -159,36 +159,53 @@ impl APIHandler {
|
|||||||
authorized_pubkey,
|
authorized_pubkey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Router
|
// MARK: - Router
|
||||||
|
|
||||||
async fn handle_parsed_http_request(
|
async fn handle_parsed_http_request(
|
||||||
&self,
|
&self,
|
||||||
parsed_request: &ParsedRequest,
|
parsed_request: &ParsedRequest,
|
||||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||||
|
if let Some(url_params) = route_match(
|
||||||
if let Some(url_params) = route_match(&Method::PUT, "/user-info/:pubkey/:deviceToken", &parsed_request) {
|
&Method::PUT,
|
||||||
|
"/user-info/:pubkey/:deviceToken",
|
||||||
|
&parsed_request,
|
||||||
|
) {
|
||||||
return self.handle_user_info(parsed_request, &url_params).await;
|
return self.handle_user_info(parsed_request, &url_params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url_params) = route_match(&Method::DELETE, "/user-info/:pubkey/:deviceToken", &parsed_request) {
|
if let Some(url_params) = route_match(
|
||||||
return self.handle_user_info_remove(parsed_request, &url_params).await;
|
&Method::DELETE,
|
||||||
|
"/user-info/:pubkey/:deviceToken",
|
||||||
|
&parsed_request,
|
||||||
|
) {
|
||||||
|
return self
|
||||||
|
.handle_user_info_remove(parsed_request, &url_params)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url_params) = route_match(&Method::GET, "/user-info/:pubkey/:deviceToken/preferences", &parsed_request) {
|
if let Some(url_params) = route_match(
|
||||||
|
&Method::GET,
|
||||||
|
"/user-info/:pubkey/:deviceToken/preferences",
|
||||||
|
&parsed_request,
|
||||||
|
) {
|
||||||
return self.get_user_settings(parsed_request, &url_params).await;
|
return self.get_user_settings(parsed_request, &url_params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url_params) = route_match(&Method::PUT, "/user-info/:pubkey/:deviceToken/preferences", &parsed_request) {
|
if let Some(url_params) = route_match(
|
||||||
|
&Method::PUT,
|
||||||
|
"/user-info/:pubkey/:deviceToken/preferences",
|
||||||
|
&parsed_request,
|
||||||
|
) {
|
||||||
return self.set_user_settings(parsed_request, &url_params).await;
|
return self.set_user_settings(parsed_request, &url_params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(APIResponse {
|
Ok(APIResponse {
|
||||||
status: StatusCode::NOT_FOUND,
|
status: StatusCode::NOT_FOUND,
|
||||||
body: json!({ "error": "Not found" }),
|
body: json!({ "error": "Not found" }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Authentication
|
// MARK: - Authentication
|
||||||
|
|
||||||
async fn authenticate(
|
async fn authenticate(
|
||||||
@ -209,7 +226,7 @@ impl APIHandler {
|
|||||||
)
|
)
|
||||||
.await)
|
.await)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Endpoint handlers
|
// MARK: - Endpoint handlers
|
||||||
|
|
||||||
async fn handle_user_info(
|
async fn handle_user_info(
|
||||||
@ -220,30 +237,36 @@ impl APIHandler {
|
|||||||
// Early return if `deviceToken` is missing
|
// Early return if `deviceToken` is missing
|
||||||
let device_token = match url_params.get("deviceToken") {
|
let device_token = match url_params.get("deviceToken") {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` is missing
|
// Early return if `pubkey` is missing
|
||||||
let pubkey = match url_params.get("pubkey") {
|
let pubkey = match url_params.get("pubkey") {
|
||||||
Some(key) => key,
|
Some(key) => key,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "pubkey is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "pubkey is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the `pubkey` and prepare it for use
|
// Validate the `pubkey` and prepare it for use
|
||||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
Err(_) => return Ok(APIResponse {
|
Err(_) => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "Invalid pubkey" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "Invalid pubkey" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||||
if pubkey != req.authorized_pubkey {
|
if pubkey != req.authorized_pubkey {
|
||||||
return Ok(APIResponse {
|
return Ok(APIResponse {
|
||||||
@ -251,9 +274,11 @@ impl APIHandler {
|
|||||||
body: json!({ "error": "Forbidden" }),
|
body: json!({ "error": "Forbidden" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with the main logic after passing all checks
|
// Proceed with the main logic after passing all checks
|
||||||
self.notification_manager.save_user_device_info_if_not_present(pubkey, device_token).await?;
|
self.notification_manager
|
||||||
|
.save_user_device_info_if_not_present(pubkey, device_token)
|
||||||
|
.await?;
|
||||||
Ok(APIResponse {
|
Ok(APIResponse {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
body: json!({ "message": "User info saved successfully" }),
|
body: json!({ "message": "User info saved successfully" }),
|
||||||
@ -268,30 +293,36 @@ impl APIHandler {
|
|||||||
// Early return if `deviceToken` is missing
|
// Early return if `deviceToken` is missing
|
||||||
let device_token = match url_params.get("deviceToken") {
|
let device_token = match url_params.get("deviceToken") {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` is missing
|
// Early return if `pubkey` is missing
|
||||||
let pubkey = match url_params.get("pubkey") {
|
let pubkey = match url_params.get("pubkey") {
|
||||||
Some(key) => key,
|
Some(key) => key,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "pubkey is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "pubkey is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the `pubkey` and prepare it for use
|
// Validate the `pubkey` and prepare it for use
|
||||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
Err(_) => return Ok(APIResponse {
|
Err(_) => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "Invalid pubkey" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "Invalid pubkey" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||||
if pubkey != req.authorized_pubkey {
|
if pubkey != req.authorized_pubkey {
|
||||||
return Ok(APIResponse {
|
return Ok(APIResponse {
|
||||||
@ -299,16 +330,18 @@ impl APIHandler {
|
|||||||
body: json!({ "error": "Forbidden" }),
|
body: json!({ "error": "Forbidden" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with the main logic after passing all checks
|
// Proceed with the main logic after passing all checks
|
||||||
self.notification_manager.remove_user_device_info(pubkey, device_token).await?;
|
self.notification_manager
|
||||||
|
.remove_user_device_info(pubkey, device_token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(APIResponse {
|
Ok(APIResponse {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
body: json!({ "message": "User info removed successfully" }),
|
body: json!({ "message": "User info removed successfully" }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_user_settings(
|
async fn set_user_settings(
|
||||||
&self,
|
&self,
|
||||||
req: &ParsedRequest,
|
req: &ParsedRequest,
|
||||||
@ -317,30 +350,36 @@ impl APIHandler {
|
|||||||
// Early return if `deviceToken` is missing
|
// Early return if `deviceToken` is missing
|
||||||
let device_token = match url_params.get("deviceToken") {
|
let device_token = match url_params.get("deviceToken") {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` is missing
|
// Early return if `pubkey` is missing
|
||||||
let pubkey = match url_params.get("pubkey") {
|
let pubkey = match url_params.get("pubkey") {
|
||||||
Some(key) => key,
|
Some(key) => key,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "pubkey is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "pubkey is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the `pubkey` and prepare it for use
|
// Validate the `pubkey` and prepare it for use
|
||||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
Err(_) => return Ok(APIResponse {
|
Err(_) => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "Invalid pubkey" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "Invalid pubkey" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||||
if pubkey != req.authorized_pubkey {
|
if pubkey != req.authorized_pubkey {
|
||||||
return Ok(APIResponse {
|
return Ok(APIResponse {
|
||||||
@ -348,7 +387,7 @@ impl APIHandler {
|
|||||||
body: json!({ "error": "Forbidden" }),
|
body: json!({ "error": "Forbidden" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with the main logic after passing all checks
|
// Proceed with the main logic after passing all checks
|
||||||
let body = req.body_json()?;
|
let body = req.body_json()?;
|
||||||
|
|
||||||
@ -361,14 +400,20 @@ impl APIHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.notification_manager.save_user_notification_settings(&req.authorized_pubkey, device_token.to_string(), settings).await?;
|
self.notification_manager
|
||||||
|
.save_user_notification_settings(
|
||||||
|
&req.authorized_pubkey,
|
||||||
|
device_token.to_string(),
|
||||||
|
settings,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
return Ok(APIResponse {
|
return Ok(APIResponse {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
body: json!({ "message": "User settings saved successfully" }),
|
body: json!({ "message": "User settings saved successfully" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_settings(
|
async fn get_user_settings(
|
||||||
&self,
|
&self,
|
||||||
req: &ParsedRequest,
|
req: &ParsedRequest,
|
||||||
@ -377,30 +422,36 @@ impl APIHandler {
|
|||||||
// Early return if `deviceToken` is missing
|
// Early return if `deviceToken` is missing
|
||||||
let device_token = match url_params.get("deviceToken") {
|
let device_token = match url_params.get("deviceToken") {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` is missing
|
// Early return if `pubkey` is missing
|
||||||
let pubkey = match url_params.get("pubkey") {
|
let pubkey = match url_params.get("pubkey") {
|
||||||
Some(key) => key,
|
Some(key) => key,
|
||||||
None => return Ok(APIResponse {
|
None => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "pubkey is required on the URL" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "pubkey is required on the URL" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the `pubkey` and prepare it for use
|
// Validate the `pubkey` and prepare it for use
|
||||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
Err(_) => return Ok(APIResponse {
|
Err(_) => {
|
||||||
status: StatusCode::BAD_REQUEST,
|
return Ok(APIResponse {
|
||||||
body: json!({ "error": "Invalid pubkey" }),
|
status: StatusCode::BAD_REQUEST,
|
||||||
}),
|
body: json!({ "error": "Invalid pubkey" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||||
if pubkey != req.authorized_pubkey {
|
if pubkey != req.authorized_pubkey {
|
||||||
return Ok(APIResponse {
|
return Ok(APIResponse {
|
||||||
@ -408,10 +459,13 @@ impl APIHandler {
|
|||||||
body: json!({ "error": "Forbidden" }),
|
body: json!({ "error": "Forbidden" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with the main logic after passing all checks
|
// Proceed with the main logic after passing all checks
|
||||||
let settings = self.notification_manager.get_user_notification_settings(&req.authorized_pubkey, device_token.to_string()).await?;
|
let settings = self
|
||||||
|
.notification_manager
|
||||||
|
.get_user_notification_settings(&req.authorized_pubkey, device_token.to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(APIResponse {
|
Ok(APIResponse {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
body: json!(settings),
|
body: json!(settings),
|
||||||
@ -462,10 +516,14 @@ struct APIResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper functions
|
// MARK: - Helper functions
|
||||||
|
|
||||||
/// Matches the request to a specified route, returning a hashmap of the route parameters
|
/// Matches the request to a specified route, returning a hashmap of the route parameters
|
||||||
/// e.g. GET /user/:id/info route against request GET /user/123/info matches to { "id": "123" }
|
/// e.g. GET /user/:id/info route against request GET /user/123/info matches to { "id": "123" }
|
||||||
fn route_match<'a>(method: &Method, path: &'a str, req: &ParsedRequest) -> Option<HashMap<&'a str, String>> {
|
fn route_match<'a>(
|
||||||
|
method: &Method,
|
||||||
|
path: &'a str,
|
||||||
|
req: &ParsedRequest,
|
||||||
|
) -> Option<HashMap<&'a str, String>> {
|
||||||
if method != req.method {
|
if method != req.method {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
use super::utils::time_delta::TimeDelta;
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use nostr;
|
use nostr;
|
||||||
use nostr::bitcoin::hashes::sha256::Hash as Sha256Hash;
|
use nostr::bitcoin::hashes::sha256::Hash as Sha256Hash;
|
||||||
use nostr::bitcoin::hashes::Hash;
|
use nostr::bitcoin::hashes::Hash;
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use super::utils::time_delta::TimeDelta;
|
|
||||||
|
|
||||||
pub async fn nip98_verify_auth_header(
|
pub async fn nip98_verify_auth_header(
|
||||||
auth_header: String,
|
auth_header: String,
|
||||||
|
@ -54,7 +54,9 @@ impl NotePushEnv {
|
|||||||
.unwrap_or(DEFAULT_NOSTR_EVENT_CACHE_MAX_AGE.to_string())
|
.unwrap_or(DEFAULT_NOSTR_EVENT_CACHE_MAX_AGE.to_string())
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.map(|s| std::time::Duration::from_secs(s))
|
.map(|s| std::time::Duration::from_secs(s))
|
||||||
.unwrap_or(std::time::Duration::from_secs(DEFAULT_NOSTR_EVENT_CACHE_MAX_AGE));
|
.unwrap_or(std::time::Duration::from_secs(
|
||||||
|
DEFAULT_NOSTR_EVENT_CACHE_MAX_AGE,
|
||||||
|
));
|
||||||
|
|
||||||
Ok(NotePushEnv {
|
Ok(NotePushEnv {
|
||||||
apns_private_key_path,
|
apns_private_key_path,
|
||||||
@ -67,7 +69,7 @@ impl NotePushEnv {
|
|||||||
port,
|
port,
|
||||||
api_base_url,
|
api_base_url,
|
||||||
relay_url,
|
relay_url,
|
||||||
nostr_event_cache_max_age
|
nostr_event_cache_max_age,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
pub mod nostr_network_helper;
|
|
||||||
mod nostr_event_extensions;
|
|
||||||
mod nostr_event_cache;
|
mod nostr_event_cache;
|
||||||
|
mod nostr_event_extensions;
|
||||||
|
pub mod nostr_network_helper;
|
||||||
pub mod notification_manager;
|
pub mod notification_manager;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use crate::{notification_manager::nostr_event_extensions::MaybeConvertibleToTimestampedMuteList, utils::time_delta::TimeDelta};
|
use super::nostr_event_extensions::{MaybeConvertibleToRelayList, RelayList, TimestampedMuteList};
|
||||||
use tokio::time::{Duration, Instant};
|
use crate::{
|
||||||
|
notification_manager::nostr_event_extensions::MaybeConvertibleToTimestampedMuteList,
|
||||||
|
utils::time_delta::TimeDelta,
|
||||||
|
};
|
||||||
|
use log;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use log;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use super::nostr_event_extensions::{MaybeConvertibleToRelayList, RelayList, TimestampedMuteList};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
struct CacheEntry<T> {
|
struct CacheEntry<T> {
|
||||||
value: Option<T>, // `None` means the event does not exist as far as we know (It does NOT mean expired)
|
value: Option<T>, // `None` means the event does not exist as far as we know (It does NOT mean expired)
|
||||||
added_at: Instant,
|
added_at: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +21,10 @@ impl<T> CacheEntry<T> {
|
|||||||
|
|
||||||
pub fn new(value: T) -> Self {
|
pub fn new(value: T) -> Self {
|
||||||
let added_at = Instant::now();
|
let added_at = Instant::now();
|
||||||
CacheEntry { value: Some(value), added_at }
|
CacheEntry {
|
||||||
|
value: Some(value),
|
||||||
|
added_at,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maybe(value: Option<T>) -> Self {
|
pub fn maybe(value: Option<T>) -> Self {
|
||||||
@ -28,7 +34,10 @@ impl<T> CacheEntry<T> {
|
|||||||
|
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
let added_at = Instant::now();
|
let added_at = Instant::now();
|
||||||
CacheEntry { value: None, added_at }
|
CacheEntry {
|
||||||
|
value: None,
|
||||||
|
added_at,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> Option<&T> {
|
pub fn value(&self) -> Option<&T> {
|
||||||
@ -44,12 +53,21 @@ pub struct Cache {
|
|||||||
max_age: Duration,
|
max_age: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cache_entry<T: Clone>(list: &mut HashMap<PublicKey, CacheEntry<T>>, pubkey: &PublicKey, max_age: Duration, name: &str) -> Result<Option<T>, CacheError> {
|
fn get_cache_entry<T: Clone>(
|
||||||
|
list: &mut HashMap<PublicKey, CacheEntry<T>>,
|
||||||
|
pubkey: &PublicKey,
|
||||||
|
max_age: Duration,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<T>, CacheError> {
|
||||||
let res = if let Some(entry) = list.get(pubkey) {
|
let res = if let Some(entry) = list.get(pubkey) {
|
||||||
if !entry.is_expired(max_age) {
|
if !entry.is_expired(max_age) {
|
||||||
Ok(entry.value().cloned())
|
Ok(entry.value().cloned())
|
||||||
} else {
|
} else {
|
||||||
log::debug!("{} list for pubkey {} is expired, removing it from the cache", name, pubkey.to_hex());
|
log::debug!(
|
||||||
|
"{} list for pubkey {} is expired, removing it from the cache",
|
||||||
|
name,
|
||||||
|
pubkey.to_hex()
|
||||||
|
);
|
||||||
Err(CacheError::Expired)
|
Err(CacheError::Expired)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -78,62 +96,90 @@ impl Cache {
|
|||||||
|
|
||||||
// MARK: - Adding items to the cache
|
// MARK: - Adding items to the cache
|
||||||
|
|
||||||
pub fn add_optional_mute_list_with_author<'a>(&'a mut self, author: &PublicKey, mute_list: Option<&Event>) {
|
pub fn add_optional_mute_list_with_author<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
author: &PublicKey,
|
||||||
|
mute_list: Option<&Event>,
|
||||||
|
) {
|
||||||
if let Some(mute_list) = mute_list {
|
if let Some(mute_list) = mute_list {
|
||||||
self.add_event(mute_list);
|
self.add_event(mute_list);
|
||||||
} else {
|
} else {
|
||||||
self.mute_lists.insert(
|
self.mute_lists
|
||||||
author.to_owned(),
|
.insert(author.to_owned(), CacheEntry::empty());
|
||||||
CacheEntry::empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_optional_relay_list_with_author<'a>(&'a mut self, author: &PublicKey, relay_list_event: Option<&Event>) {
|
pub fn add_optional_relay_list_with_author<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
author: &PublicKey,
|
||||||
|
relay_list_event: Option<&Event>,
|
||||||
|
) {
|
||||||
if let Some(relay_list_event) = relay_list_event {
|
if let Some(relay_list_event) = relay_list_event {
|
||||||
self.add_event(relay_list_event);
|
self.add_event(relay_list_event);
|
||||||
} else {
|
} else {
|
||||||
self.relay_lists.insert(
|
self.relay_lists
|
||||||
author.to_owned(),
|
.insert(author.to_owned(), CacheEntry::empty());
|
||||||
CacheEntry::empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_optional_contact_list_with_author<'a>(&'a mut self, author: &PublicKey, contact_list: Option<&Event>) {
|
pub fn add_optional_contact_list_with_author<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
author: &PublicKey,
|
||||||
|
contact_list: Option<&Event>,
|
||||||
|
) {
|
||||||
if let Some(contact_list) = contact_list {
|
if let Some(contact_list) = contact_list {
|
||||||
self.add_event(contact_list);
|
self.add_event(contact_list);
|
||||||
} else {
|
} else {
|
||||||
self.contact_lists.insert(
|
self.contact_lists
|
||||||
author.to_owned(),
|
.insert(author.to_owned(), CacheEntry::empty());
|
||||||
CacheEntry::empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_event(&mut self, event: &Event) {
|
pub fn add_event(&mut self, event: &Event) {
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::MuteList => {
|
Kind::MuteList => {
|
||||||
self.mute_lists.insert(event.pubkey.clone(), CacheEntry::maybe(event.to_timestamped_mute_list()));
|
self.mute_lists.insert(
|
||||||
log::debug!("Added mute list to the cache. Event ID: {}", event.id.to_hex());
|
event.pubkey.clone(),
|
||||||
|
CacheEntry::maybe(event.to_timestamped_mute_list()),
|
||||||
|
);
|
||||||
|
log::debug!(
|
||||||
|
"Added mute list to the cache. Event ID: {}",
|
||||||
|
event.id.to_hex()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Kind::ContactList => {
|
Kind::ContactList => {
|
||||||
log::debug!("Added contact list to the cache. Event ID: {}", event.id.to_hex());
|
log::debug!(
|
||||||
self.contact_lists.insert(event.pubkey.clone(), CacheEntry::new(event.to_owned()));
|
"Added contact list to the cache. Event ID: {}",
|
||||||
},
|
event.id.to_hex()
|
||||||
|
);
|
||||||
|
self.contact_lists
|
||||||
|
.insert(event.pubkey.clone(), CacheEntry::new(event.to_owned()));
|
||||||
|
}
|
||||||
Kind::RelayList => {
|
Kind::RelayList => {
|
||||||
log::debug!("Added relay list to the cache. Event ID: {}", event.id.to_hex());
|
log::debug!(
|
||||||
self.relay_lists.insert(event.pubkey.clone(), CacheEntry::maybe(event.to_relay_list()));
|
"Added relay list to the cache. Event ID: {}",
|
||||||
},
|
event.id.to_hex()
|
||||||
|
);
|
||||||
|
self.relay_lists.insert(
|
||||||
|
event.pubkey.clone(),
|
||||||
|
CacheEntry::maybe(event.to_relay_list()),
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("Unknown event kind, not adding to any cache. Event ID: {}", event.id.to_hex());
|
log::debug!(
|
||||||
|
"Unknown event kind, not adding to any cache. Event ID: {}",
|
||||||
|
event.id.to_hex()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetching items from the cache
|
// MARK: - Fetching items from the cache
|
||||||
|
|
||||||
pub fn get_mute_list(&mut self, pubkey: &PublicKey) -> Result<Option<TimestampedMuteList>, CacheError> {
|
pub fn get_mute_list(
|
||||||
|
&mut self,
|
||||||
|
pubkey: &PublicKey,
|
||||||
|
) -> Result<Option<TimestampedMuteList>, CacheError> {
|
||||||
get_cache_entry(&mut self.mute_lists, pubkey, self.max_age, "Mute")
|
get_cache_entry(&mut self.mute_lists, pubkey, self.max_age, "Mute")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +209,9 @@ mod tests {
|
|||||||
// Helper function to create a dummy event of a given kind for testing.
|
// Helper function to create a dummy event of a given kind for testing.
|
||||||
fn create_dummy_event(pubkey: PublicKey, kind: Kind) -> Event {
|
fn create_dummy_event(pubkey: PublicKey, kind: Kind) -> Event {
|
||||||
// In a real test, you might generate keys or events more dynamically.
|
// In a real test, you might generate keys or events more dynamically.
|
||||||
let id = EventId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
|
let id =
|
||||||
|
EventId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||||
|
.unwrap();
|
||||||
let created_at = Timestamp::now();
|
let created_at = Timestamp::now();
|
||||||
let content = "";
|
let content = "";
|
||||||
let sig_str = "8e1a61523765a6e577e3ca0c87afe3694ed518719aea067701c35262dd2a3c7e3ca0946fe98463a3af706dd333695ceec6cb3b29254c557c8630d3db1171ea3d";
|
let sig_str = "8e1a61523765a6e577e3ca0c87afe3694ed518719aea067701c35262dd2a3c7e3ca0946fe98463a3af706dd333695ceec6cb3b29254c557c8630d3db1171ea3d";
|
||||||
@ -175,7 +223,8 @@ mod tests {
|
|||||||
// Helper function to create a dummy public key for testing.
|
// Helper function to create a dummy public key for testing.
|
||||||
fn create_dummy_pubkey() -> PublicKey {
|
fn create_dummy_pubkey() -> PublicKey {
|
||||||
// In a real project, you'd generate a key. For the sake of tests, just parse a known hex.
|
// In a real project, you'd generate a key. For the sake of tests, just parse a known hex.
|
||||||
PublicKey::from_hex("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245").unwrap()
|
PublicKey::from_hex("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -185,7 +234,10 @@ mod tests {
|
|||||||
let mut cache = Cache::new(max_age);
|
let mut cache = Cache::new(max_age);
|
||||||
|
|
||||||
// Initially, no contact list should be found.
|
// Initially, no contact list should be found.
|
||||||
assert!(matches!(cache.get_contact_list(&pubkey), Err(CacheError::NotFound)));
|
assert!(matches!(
|
||||||
|
cache.get_contact_list(&pubkey),
|
||||||
|
Err(CacheError::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
// Add a contact list event.
|
// Add a contact list event.
|
||||||
let event = create_dummy_event(pubkey, Kind::ContactList);
|
let event = create_dummy_event(pubkey, Kind::ContactList);
|
||||||
@ -205,7 +257,10 @@ mod tests {
|
|||||||
let mut cache = Cache::new(max_age);
|
let mut cache = Cache::new(max_age);
|
||||||
|
|
||||||
// No mute list initially
|
// No mute list initially
|
||||||
assert!(matches!(cache.get_mute_list(&pubkey), Err(CacheError::NotFound)));
|
assert!(matches!(
|
||||||
|
cache.get_mute_list(&pubkey),
|
||||||
|
Err(CacheError::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
// Add a mute list event.
|
// Add a mute list event.
|
||||||
let mutelist_event = {
|
let mutelist_event = {
|
||||||
@ -225,7 +280,10 @@ mod tests {
|
|||||||
let mut cache = Cache::new(max_age);
|
let mut cache = Cache::new(max_age);
|
||||||
|
|
||||||
// No relay list initially
|
// No relay list initially
|
||||||
assert!(matches!(cache.get_relay_list(&pubkey), Err(CacheError::NotFound)));
|
assert!(matches!(
|
||||||
|
cache.get_relay_list(&pubkey),
|
||||||
|
Err(CacheError::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
// Add a relay list event.
|
// Add a relay list event.
|
||||||
let relaylist_event = create_dummy_event(pubkey, Kind::RelayList);
|
let relaylist_event = create_dummy_event(pubkey, Kind::RelayList);
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
use nostr::{self, key::PublicKey, nips::{nip51::MuteList, nip65}, Alphabet, SingleLetterTag, TagKind::SingleLetter};
|
use nostr::{
|
||||||
|
self,
|
||||||
|
key::PublicKey,
|
||||||
|
nips::{nip51::MuteList, nip65},
|
||||||
|
Alphabet, SingleLetterTag,
|
||||||
|
TagKind::SingleLetter,
|
||||||
|
};
|
||||||
use nostr_sdk::{EventId, Kind, TagKind};
|
use nostr_sdk::{EventId, Kind, TagKind};
|
||||||
|
|
||||||
/// Temporary scaffolding of old methods that have not been ported to use native Event methods
|
/// Temporary scaffolding of old methods that have not been ported to use native Event methods
|
||||||
@ -104,10 +110,26 @@ impl MaybeConvertibleToMuteList for nostr::Event {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(MuteList {
|
Some(MuteList {
|
||||||
public_keys: self.referenced_pubkeys().iter().map(|pk| pk.clone()).collect(),
|
public_keys: self
|
||||||
hashtags: self.referenced_hashtags().iter().map(|tag| tag.clone()).collect(),
|
.referenced_pubkeys()
|
||||||
event_ids: self.referenced_event_ids().iter().map(|id| id.clone()).collect(),
|
.iter()
|
||||||
words: self.get_tags_content(TagKind::Word).iter().map(|tag| tag.to_string()).collect(),
|
.map(|pk| pk.clone())
|
||||||
|
.collect(),
|
||||||
|
hashtags: self
|
||||||
|
.referenced_hashtags()
|
||||||
|
.iter()
|
||||||
|
.map(|tag| tag.clone())
|
||||||
|
.collect(),
|
||||||
|
event_ids: self
|
||||||
|
.referenced_event_ids()
|
||||||
|
.iter()
|
||||||
|
.map(|id| id.clone())
|
||||||
|
.collect(),
|
||||||
|
words: self
|
||||||
|
.get_tags_content(TagKind::Word)
|
||||||
|
.iter()
|
||||||
|
.map(|tag| tag.to_string())
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +160,8 @@ impl MaybeConvertibleToRelayList for nostr::Event {
|
|||||||
}
|
}
|
||||||
let extracted_relay_list = nip65::extract_relay_list(&self);
|
let extracted_relay_list = nip65::extract_relay_list(&self);
|
||||||
// Convert the extracted relay list data fully into owned data that can be returned
|
// Convert the extracted relay list data fully into owned data that can be returned
|
||||||
let extracted_relay_list_owned = extracted_relay_list.into_iter()
|
let extracted_relay_list_owned = extracted_relay_list
|
||||||
|
.into_iter()
|
||||||
.map(|(url, metadata)| (url.clone(), metadata.as_ref().map(|m| m.clone())))
|
.map(|(url, metadata)| (url.clone(), metadata.as_ref().map(|m| m.clone())))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -167,45 +190,60 @@ impl Codable for MuteList {
|
|||||||
|
|
||||||
fn from_json(json: serde_json::Value) -> Result<Self, Box<dyn std::error::Error>>
|
fn from_json(json: serde_json::Value) -> Result<Self, Box<dyn std::error::Error>>
|
||||||
where
|
where
|
||||||
Self: Sized {
|
Self: Sized,
|
||||||
let public_keys = json.get("public_keys")
|
{
|
||||||
.ok_or_else(|| "Missing 'public_keys' field".to_string())?
|
let public_keys = json
|
||||||
.as_array()
|
.get("public_keys")
|
||||||
.ok_or_else(|| "'public_keys' must be an array".to_string())?
|
.ok_or_else(|| "Missing 'public_keys' field".to_string())?
|
||||||
.iter()
|
.as_array()
|
||||||
.map(|pk| PublicKey::from_hex(pk.as_str().unwrap_or_default()).map_err(|e| e.to_string()))
|
.ok_or_else(|| "'public_keys' must be an array".to_string())?
|
||||||
.collect::<Result<Vec<PublicKey>, String>>()?;
|
.iter()
|
||||||
|
.map(|pk| {
|
||||||
let hashtags = json.get("hashtags")
|
PublicKey::from_hex(pk.as_str().unwrap_or_default()).map_err(|e| e.to_string())
|
||||||
.ok_or_else(|| "Missing 'hashtags' field".to_string())?
|
|
||||||
.as_array()
|
|
||||||
.ok_or_else(|| "'hashtags' must be an array".to_string())?
|
|
||||||
.iter()
|
|
||||||
.map(|tag| tag.as_str().map(|s| s.to_string()).ok_or_else(|| "Invalid hashtag".to_string()))
|
|
||||||
.collect::<Result<Vec<String>, String>>()?;
|
|
||||||
|
|
||||||
let event_ids = json.get("event_ids")
|
|
||||||
.ok_or_else(|| "Missing 'event_ids' field".to_string())?
|
|
||||||
.as_array()
|
|
||||||
.ok_or_else(|| "'event_ids' must be an array".to_string())?
|
|
||||||
.iter()
|
|
||||||
.map(|id| EventId::from_hex(id.as_str().unwrap_or_default()).map_err(|e| e.to_string()))
|
|
||||||
.collect::<Result<Vec<EventId>, String>>()?;
|
|
||||||
|
|
||||||
let words = json.get("words")
|
|
||||||
.ok_or_else(|| "Missing 'words' field".to_string())?
|
|
||||||
.as_array()
|
|
||||||
.ok_or_else(|| "'words' must be an array".to_string())?
|
|
||||||
.iter()
|
|
||||||
.map(|word| word.as_str().map(|s| s.to_string()).ok_or_else(|| "Invalid word".to_string()))
|
|
||||||
.collect::<Result<Vec<String>, String>>()?;
|
|
||||||
|
|
||||||
Ok(MuteList {
|
|
||||||
public_keys,
|
|
||||||
hashtags,
|
|
||||||
event_ids,
|
|
||||||
words,
|
|
||||||
})
|
})
|
||||||
|
.collect::<Result<Vec<PublicKey>, String>>()?;
|
||||||
|
|
||||||
|
let hashtags = json
|
||||||
|
.get("hashtags")
|
||||||
|
.ok_or_else(|| "Missing 'hashtags' field".to_string())?
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| "'hashtags' must be an array".to_string())?
|
||||||
|
.iter()
|
||||||
|
.map(|tag| {
|
||||||
|
tag.as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| "Invalid hashtag".to_string())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<String>, String>>()?;
|
||||||
|
|
||||||
|
let event_ids = json
|
||||||
|
.get("event_ids")
|
||||||
|
.ok_or_else(|| "Missing 'event_ids' field".to_string())?
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| "'event_ids' must be an array".to_string())?
|
||||||
|
.iter()
|
||||||
|
.map(|id| EventId::from_hex(id.as_str().unwrap_or_default()).map_err(|e| e.to_string()))
|
||||||
|
.collect::<Result<Vec<EventId>, String>>()?;
|
||||||
|
|
||||||
|
let words = json
|
||||||
|
.get("words")
|
||||||
|
.ok_or_else(|| "Missing 'words' field".to_string())?
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| "'words' must be an array".to_string())?
|
||||||
|
.iter()
|
||||||
|
.map(|word| {
|
||||||
|
word.as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| "Invalid word".to_string())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<String>, String>>()?;
|
||||||
|
|
||||||
|
Ok(MuteList {
|
||||||
|
public_keys,
|
||||||
|
hashtags,
|
||||||
|
event_ids,
|
||||||
|
words,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use tokio::sync::Mutex;
|
use super::nostr_event_cache::Cache;
|
||||||
use super::nostr_event_extensions::{RelayList, TimestampedMuteList};
|
use super::nostr_event_extensions::{RelayList, TimestampedMuteList};
|
||||||
use super::notification_manager::EventSaver;
|
use super::notification_manager::EventSaver;
|
||||||
use super::ExtendedEvent;
|
use super::ExtendedEvent;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use super::nostr_event_cache::Cache;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
@ -11,13 +11,17 @@ const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
pub struct NostrNetworkHelper {
|
pub struct NostrNetworkHelper {
|
||||||
bootstrap_client: Client,
|
bootstrap_client: Client,
|
||||||
cache: Mutex<Cache>,
|
cache: Mutex<Cache>,
|
||||||
event_saver: EventSaver
|
event_saver: EventSaver,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrNetworkHelper {
|
impl NostrNetworkHelper {
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
pub async fn new(relay_url: String, cache_max_age: Duration, event_saver: EventSaver) -> Result<Self, Box<dyn std::error::Error>> {
|
pub async fn new(
|
||||||
|
relay_url: String,
|
||||||
|
cache_max_age: Duration,
|
||||||
|
event_saver: EventSaver,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let client = Client::new(&Keys::generate());
|
let client = Client::new(&Keys::generate());
|
||||||
client.add_relay(relay_url.clone()).await?;
|
client.add_relay(relay_url.clone()).await?;
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
@ -54,7 +58,7 @@ impl NostrNetworkHelper {
|
|||||||
if let Ok(optional_mute_list) = cache_mutex_guard.get_mute_list(pubkey) {
|
if let Ok(optional_mute_list) = cache_mutex_guard.get_mute_list(pubkey) {
|
||||||
return optional_mute_list;
|
return optional_mute_list;
|
||||||
}
|
}
|
||||||
} // Release the lock here for improved performance
|
} // Release the lock here for improved performance
|
||||||
|
|
||||||
// We don't have an answer from the cache, so we need to fetch it
|
// We don't have an answer from the cache, so we need to fetch it
|
||||||
let mute_list_event = self.fetch_single_event(pubkey, Kind::MuteList).await;
|
let mute_list_event = self.fetch_single_event(pubkey, Kind::MuteList).await;
|
||||||
@ -69,10 +73,15 @@ impl NostrNetworkHelper {
|
|||||||
if let Ok(optional_relay_list) = cache_mutex_guard.get_relay_list(pubkey) {
|
if let Ok(optional_relay_list) = cache_mutex_guard.get_relay_list(pubkey) {
|
||||||
return optional_relay_list;
|
return optional_relay_list;
|
||||||
}
|
}
|
||||||
} // Release the lock here for improved performance
|
} // Release the lock here for improved performance
|
||||||
|
|
||||||
// We don't have an answer from the cache, so we need to fetch it
|
// We don't have an answer from the cache, so we need to fetch it
|
||||||
let relay_list_event = NostrNetworkHelper::fetch_single_event_from_client(pubkey, Kind::RelayList, &self.bootstrap_client).await;
|
let relay_list_event = NostrNetworkHelper::fetch_single_event_from_client(
|
||||||
|
pubkey,
|
||||||
|
Kind::RelayList,
|
||||||
|
&self.bootstrap_client,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let mut cache_mutex_guard = self.cache.lock().await;
|
let mut cache_mutex_guard = self.cache.lock().await;
|
||||||
cache_mutex_guard.add_optional_relay_list_with_author(pubkey, relay_list_event.as_ref());
|
cache_mutex_guard.add_optional_relay_list_with_author(pubkey, relay_list_event.as_ref());
|
||||||
cache_mutex_guard.get_relay_list(pubkey).ok()?
|
cache_mutex_guard.get_relay_list(pubkey).ok()?
|
||||||
@ -84,12 +93,13 @@ impl NostrNetworkHelper {
|
|||||||
if let Ok(optional_contact_list) = cache_mutex_guard.get_contact_list(pubkey) {
|
if let Ok(optional_contact_list) = cache_mutex_guard.get_contact_list(pubkey) {
|
||||||
return optional_contact_list;
|
return optional_contact_list;
|
||||||
}
|
}
|
||||||
} // Release the lock here for improved performance
|
} // Release the lock here for improved performance
|
||||||
|
|
||||||
// We don't have an answer from the cache, so we need to fetch it
|
// We don't have an answer from the cache, so we need to fetch it
|
||||||
let contact_list_event = self.fetch_single_event(pubkey, Kind::ContactList).await;
|
let contact_list_event = self.fetch_single_event(pubkey, Kind::ContactList).await;
|
||||||
let mut cache_mutex_guard = self.cache.lock().await;
|
let mut cache_mutex_guard = self.cache.lock().await;
|
||||||
cache_mutex_guard.add_optional_contact_list_with_author(pubkey, contact_list_event.as_ref());
|
cache_mutex_guard
|
||||||
|
.add_optional_contact_list_with_author(pubkey, contact_list_event.as_ref());
|
||||||
cache_mutex_guard.get_contact_list(pubkey).ok()?
|
cache_mutex_guard.get_contact_list(pubkey).ok()?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,15 +109,24 @@ impl NostrNetworkHelper {
|
|||||||
let event = match self.make_client_for(author).await {
|
let event = match self.make_client_for(author).await {
|
||||||
Some(client) => {
|
Some(client) => {
|
||||||
NostrNetworkHelper::fetch_single_event_from_client(author, kind, &client).await
|
NostrNetworkHelper::fetch_single_event_from_client(author, kind, &client).await
|
||||||
},
|
}
|
||||||
None => {
|
None => {
|
||||||
NostrNetworkHelper::fetch_single_event_from_client(author, kind, &self.bootstrap_client).await
|
NostrNetworkHelper::fetch_single_event_from_client(
|
||||||
},
|
author,
|
||||||
|
kind,
|
||||||
|
&self.bootstrap_client,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Save event to our database if needed
|
// Save event to our database if needed
|
||||||
if let Some(event) = event.clone() {
|
if let Some(event) = event.clone() {
|
||||||
if let Err(error) = self.event_saver.save_if_needed(&event).await {
|
if let Err(error) = self.event_saver.save_if_needed(&event).await {
|
||||||
log::warn!("Failed to save event '{:?}'. Error: {:?}", event.id.to_hex(), error)
|
log::warn!(
|
||||||
|
"Failed to save event '{:?}'. Error: {:?}",
|
||||||
|
event.id.to_hex(),
|
||||||
|
error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event
|
event
|
||||||
@ -118,7 +137,8 @@ impl NostrNetworkHelper {
|
|||||||
|
|
||||||
let relay_list = self.get_relay_list(author).await?;
|
let relay_list = self.get_relay_list(author).await?;
|
||||||
for (url, metadata) in relay_list {
|
for (url, metadata) in relay_list {
|
||||||
if metadata.map_or(true, |m| m == RelayMetadata::Write) { // Only add "write" relays, as per NIP-65 spec on reading data FROM user
|
if metadata.map_or(true, |m| m == RelayMetadata::Write) {
|
||||||
|
// Only add "write" relays, as per NIP-65 spec on reading data FROM user
|
||||||
if let Err(e) = client.add_relay(url.clone()).await {
|
if let Err(e) = client.add_relay(url.clone()).await {
|
||||||
log::warn!("Failed to add relay URL: {:?}, error: {:?}", url, e);
|
log::warn!("Failed to add relay URL: {:?}, error: {:?}", url, e);
|
||||||
}
|
}
|
||||||
@ -130,7 +150,11 @@ impl NostrNetworkHelper {
|
|||||||
Some(client)
|
Some(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_single_event_from_client(author: &PublicKey, kind: Kind, client: &Client) -> Option<Event> {
|
async fn fetch_single_event_from_client(
|
||||||
|
author: &PublicKey,
|
||||||
|
kind: Kind,
|
||||||
|
client: &Client,
|
||||||
|
) -> Option<Event> {
|
||||||
let subscription_filter = Filter::new()
|
let subscription_filter = Filter::new()
|
||||||
.kinds(vec![kind])
|
.kinds(vec![kind])
|
||||||
.authors(vec![author.clone()])
|
.authors(vec![author.clone()])
|
||||||
|
@ -10,10 +10,10 @@ use rusqlite;
|
|||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio;
|
use tokio;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use super::nostr_event_extensions::Codable;
|
use super::nostr_event_extensions::Codable;
|
||||||
use super::nostr_event_extensions::MaybeConvertibleToMuteList;
|
use super::nostr_event_extensions::MaybeConvertibleToMuteList;
|
||||||
@ -34,12 +34,12 @@ pub struct NotificationManager {
|
|||||||
apns_topic: String,
|
apns_topic: String,
|
||||||
apns_client: Mutex<Client>,
|
apns_client: Mutex<Client>,
|
||||||
nostr_network_helper: NostrNetworkHelper,
|
nostr_network_helper: NostrNetworkHelper,
|
||||||
pub event_saver: EventSaver
|
pub event_saver: EventSaver,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EventSaver {
|
pub struct EventSaver {
|
||||||
db: Arc<Mutex<r2d2::Pool<SqliteConnectionManager>>>
|
db: Arc<Mutex<r2d2::Pool<SqliteConnectionManager>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventSaver {
|
impl EventSaver {
|
||||||
@ -47,32 +47,46 @@ impl EventSaver {
|
|||||||
Self { db }
|
Self { db }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_if_needed(&self, event: &nostr::Event) -> Result<bool, Box<dyn std::error::Error>> {
|
pub async fn save_if_needed(
|
||||||
|
&self,
|
||||||
|
event: &nostr::Event,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
match event.to_mute_list() {
|
match event.to_mute_list() {
|
||||||
Some(mute_list) => {
|
Some(mute_list) => {
|
||||||
match self.get_saved_mute_list_for(event.author()).await.ok().flatten() {
|
match self
|
||||||
|
.get_saved_mute_list_for(event.author())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
Some(saved_timestamped_mute_list) => {
|
Some(saved_timestamped_mute_list) => {
|
||||||
let saved_mute_list_timestamp = saved_timestamped_mute_list.timestamp;
|
let saved_mute_list_timestamp = saved_timestamped_mute_list.timestamp;
|
||||||
if saved_mute_list_timestamp < event.created_at() {
|
if saved_mute_list_timestamp < event.created_at() {
|
||||||
self.save_mute_list(event.author(), mute_list, event.created_at).await?;
|
self.save_mute_list(event.author(), mute_list, event.created_at)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
return Ok(false);
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
return Ok(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
None => {
|
||||||
self.save_mute_list(event.author(), mute_list, event.created_at).await?;
|
self.save_mute_list(event.author(), mute_list, event.created_at)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
},
|
}
|
||||||
None => Ok(false),
|
None => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Muting preferences
|
// MARK: - Muting preferences
|
||||||
|
|
||||||
pub async fn save_mute_list(&self, pubkey: PublicKey, mute_list: MuteList, created_at: Timestamp) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn save_mute_list(
|
||||||
|
&self,
|
||||||
|
pubkey: PublicKey,
|
||||||
|
mute_list: MuteList,
|
||||||
|
created_at: Timestamp,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mute_list_json = mute_list.to_json()?;
|
let mute_list_json = mute_list.to_json()?;
|
||||||
let db_mutex_guard = self.db.lock().await;
|
let db_mutex_guard = self.db.lock().await;
|
||||||
let connection = db_mutex_guard.get()?;
|
let connection = db_mutex_guard.get()?;
|
||||||
@ -92,7 +106,10 @@ impl EventSaver {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_saved_mute_list_for(&self, pubkey: PublicKey) -> Result<Option<TimestampedMuteList>, Box<dyn std::error::Error>> {
|
pub async fn get_saved_mute_list_for(
|
||||||
|
&self,
|
||||||
|
pubkey: PublicKey,
|
||||||
|
) -> Result<Option<TimestampedMuteList>, Box<dyn std::error::Error>> {
|
||||||
let db_mutex_guard = self.db.lock().await;
|
let db_mutex_guard = self.db.lock().await;
|
||||||
let connection = db_mutex_guard.get()?;
|
let connection = db_mutex_guard.get()?;
|
||||||
|
|
||||||
@ -100,7 +117,10 @@ impl EventSaver {
|
|||||||
"SELECT mute_list, created_at FROM muting_preferences WHERE user_pubkey = ?",
|
"SELECT mute_list, created_at FROM muting_preferences WHERE user_pubkey = ?",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mute_list_info: (serde_json::Value, nostr::Timestamp) = match stmt.query_row([pubkey.to_sql_string()], |row| { Ok((row.get(0)?, row.get(1)?)) }) {
|
let mute_list_info: (serde_json::Value, nostr::Timestamp) = match stmt
|
||||||
|
.query_row([pubkey.to_sql_string()], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?))
|
||||||
|
}) {
|
||||||
Ok(info) => (info.0, nostr::Timestamp::from_sql_string(info.1)?),
|
Ok(info) => (info.0, nostr::Timestamp::from_sql_string(info.1)?),
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
@ -109,7 +129,7 @@ impl EventSaver {
|
|||||||
let mute_list = MuteList::from_json(mute_list_info.0)?;
|
let mute_list = MuteList::from_json(mute_list_info.0)?;
|
||||||
let timestamped_mute_list = TimestampedMuteList {
|
let timestamped_mute_list = TimestampedMuteList {
|
||||||
mute_list,
|
mute_list,
|
||||||
timestamp: mute_list_info.1
|
timestamp: mute_list_info.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(timestamped_mute_list))
|
Ok(Some(timestamped_mute_list))
|
||||||
@ -148,7 +168,12 @@ impl NotificationManager {
|
|||||||
db,
|
db,
|
||||||
apns_topic,
|
apns_topic,
|
||||||
apns_client: Mutex::new(client),
|
apns_client: Mutex::new(client),
|
||||||
nostr_network_helper: NostrNetworkHelper::new(relay_url.clone(), cache_max_age, event_saver.clone()).await?,
|
nostr_network_helper: NostrNetworkHelper::new(
|
||||||
|
relay_url.clone(),
|
||||||
|
cache_max_age,
|
||||||
|
event_saver.clone(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
event_saver,
|
event_saver,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -194,12 +219,48 @@ impl NotificationManager {
|
|||||||
|
|
||||||
// Notification settings migration (https://github.com/damus-io/damus/issues/2360)
|
// Notification settings migration (https://github.com/damus-io/damus/issues/2360)
|
||||||
|
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "zap_notifications_enabled", "BOOLEAN", Some("true"))?;
|
Self::add_column_if_not_exists(
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "mention_notifications_enabled", "BOOLEAN", Some("true"))?;
|
&db,
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "repost_notifications_enabled", "BOOLEAN", Some("true"))?;
|
"user_info",
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "reaction_notifications_enabled", "BOOLEAN", Some("true"))?;
|
"zap_notifications_enabled",
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "dm_notifications_enabled", "BOOLEAN", Some("true"))?;
|
"BOOLEAN",
|
||||||
Self::add_column_if_not_exists(&db, "user_info", "only_notifications_from_following_enabled", "BOOLEAN", Some("false"))?;
|
Some("true"),
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_not_exists(
|
||||||
|
&db,
|
||||||
|
"user_info",
|
||||||
|
"mention_notifications_enabled",
|
||||||
|
"BOOLEAN",
|
||||||
|
Some("true"),
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_not_exists(
|
||||||
|
&db,
|
||||||
|
"user_info",
|
||||||
|
"repost_notifications_enabled",
|
||||||
|
"BOOLEAN",
|
||||||
|
Some("true"),
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_not_exists(
|
||||||
|
&db,
|
||||||
|
"user_info",
|
||||||
|
"reaction_notifications_enabled",
|
||||||
|
"BOOLEAN",
|
||||||
|
Some("true"),
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_not_exists(
|
||||||
|
&db,
|
||||||
|
"user_info",
|
||||||
|
"dm_notifications_enabled",
|
||||||
|
"BOOLEAN",
|
||||||
|
Some("true"),
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_not_exists(
|
||||||
|
&db,
|
||||||
|
"user_info",
|
||||||
|
"only_notifications_from_following_enabled",
|
||||||
|
"BOOLEAN",
|
||||||
|
Some("false"),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Migration related to mute list improvements (https://github.com/damus-io/damus/issues/2118)
|
// Migration related to mute list improvements (https://github.com/damus-io/damus/issues/2118)
|
||||||
|
|
||||||
@ -232,7 +293,10 @@ impl NotificationManager {
|
|||||||
if !column_names.contains(&column_name.to_string()) {
|
if !column_names.contains(&column_name.to_string()) {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"ALTER TABLE {} ADD COLUMN {} {} {}",
|
"ALTER TABLE {} ADD COLUMN {} {} {}",
|
||||||
table_name, column_name, column_type, match default_value {
|
table_name,
|
||||||
|
column_name,
|
||||||
|
column_type,
|
||||||
|
match default_value {
|
||||||
Some(value) => format!("DEFAULT {}", value),
|
Some(value) => format!("DEFAULT {}", value),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
},
|
},
|
||||||
@ -319,12 +383,12 @@ impl NotificationManager {
|
|||||||
}
|
}
|
||||||
let pubkeys_that_received_notification =
|
let pubkeys_that_received_notification =
|
||||||
notification_status.pubkeys_that_received_notification();
|
notification_status.pubkeys_that_received_notification();
|
||||||
let relevant_pubkeys_yet_to_receive: HashSet<PublicKey> = relevant_pubkeys_that_are_registered
|
let relevant_pubkeys_yet_to_receive: HashSet<PublicKey> =
|
||||||
.difference(&pubkeys_that_received_notification)
|
relevant_pubkeys_that_are_registered
|
||||||
.filter(|&x| *x != event.pubkey)
|
.difference(&pubkeys_that_received_notification)
|
||||||
.cloned()
|
.filter(|&x| *x != event.pubkey)
|
||||||
.collect();
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut pubkeys_to_notify = HashSet::new();
|
let mut pubkeys_to_notify = HashSet::new();
|
||||||
for pubkey in relevant_pubkeys_yet_to_receive {
|
for pubkey in relevant_pubkeys_yet_to_receive {
|
||||||
@ -340,39 +404,49 @@ impl NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn should_mute_notification_for_pubkey(&self, event: &Event, pubkey: &PublicKey) -> bool {
|
async fn should_mute_notification_for_pubkey(&self, event: &Event, pubkey: &PublicKey) -> bool {
|
||||||
let latest_mute_list = self.get_newest_mute_list_available(pubkey).await.ok().flatten();
|
let latest_mute_list = self
|
||||||
|
.get_newest_mute_list_available(pubkey)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
if let Some(latest_mute_list) = latest_mute_list {
|
if let Some(latest_mute_list) = latest_mute_list {
|
||||||
return should_mute_notification_for_mutelist(event, &latest_mute_list)
|
return should_mute_notification_for_mutelist(event, &latest_mute_list);
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_newest_mute_list_available(&self, pubkey: &PublicKey) -> Result<Option<MuteList>, Box<dyn std::error::Error>> {
|
async fn get_newest_mute_list_available(
|
||||||
|
&self,
|
||||||
|
pubkey: &PublicKey,
|
||||||
|
) -> Result<Option<MuteList>, Box<dyn std::error::Error>> {
|
||||||
let timestamped_saved_mute_list = self.event_saver.get_saved_mute_list_for(*pubkey).await?;
|
let timestamped_saved_mute_list = self.event_saver.get_saved_mute_list_for(*pubkey).await?;
|
||||||
let timestamped_network_mute_list = self.nostr_network_helper.get_public_mute_list(pubkey).await;
|
let timestamped_network_mute_list =
|
||||||
Ok(match (timestamped_saved_mute_list, timestamped_network_mute_list) {
|
self.nostr_network_helper.get_public_mute_list(pubkey).await;
|
||||||
(Some(local_mute), Some(network_mute)) => {
|
Ok(
|
||||||
if local_mute.timestamp > network_mute.timestamp {
|
match (timestamped_saved_mute_list, timestamped_network_mute_list) {
|
||||||
log::debug!("Mute lists available in both database and from the network for pubkey {}. Using local mute list since it's newer.", pubkey.to_hex());
|
(Some(local_mute), Some(network_mute)) => {
|
||||||
|
if local_mute.timestamp > network_mute.timestamp {
|
||||||
|
log::debug!("Mute lists available in both database and from the network for pubkey {}. Using local mute list since it's newer.", pubkey.to_hex());
|
||||||
|
Some(local_mute.mute_list)
|
||||||
|
} else {
|
||||||
|
log::debug!("Mute lists available in both database and from the network for pubkey {}. Using network mute list since it's newer.", pubkey.to_hex());
|
||||||
|
Some(network_mute.mute_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(local_mute), None) => {
|
||||||
|
log::debug!("Mute list available in database for pubkey {}, but not from the network. Using local mute list.", pubkey.to_hex());
|
||||||
Some(local_mute.mute_list)
|
Some(local_mute.mute_list)
|
||||||
} else {
|
}
|
||||||
log::debug!("Mute lists available in both database and from the network for pubkey {}. Using network mute list since it's newer.", pubkey.to_hex());
|
(None, Some(network_mute)) => {
|
||||||
|
log::debug!("Mute list for pubkey {} available from the network, but not in the database. Using network mute list.", pubkey.to_hex());
|
||||||
Some(network_mute.mute_list)
|
Some(network_mute.mute_list)
|
||||||
}
|
}
|
||||||
|
(None, None) => {
|
||||||
|
log::debug!("No mute list available for pubkey {}", pubkey.to_hex());
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(Some(local_mute), None) => {
|
)
|
||||||
log::debug!("Mute list available in database for pubkey {}, but not from the network. Using local mute list.", pubkey.to_hex());
|
|
||||||
Some(local_mute.mute_list)
|
|
||||||
},
|
|
||||||
(None, Some(network_mute)) => {
|
|
||||||
log::debug!("Mute list for pubkey {} available from the network, but not in the database. Using network mute list.", pubkey.to_hex());
|
|
||||||
Some(network_mute.mute_list)
|
|
||||||
},
|
|
||||||
(None, None) => {
|
|
||||||
log::debug!("No mute list available for pubkey {}", pubkey.to_hex());
|
|
||||||
None
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pubkeys_relevant_to_event(
|
async fn pubkeys_relevant_to_event(
|
||||||
@ -382,8 +456,9 @@ impl NotificationManager {
|
|||||||
let mut relevant_pubkeys = event.relevant_pubkeys();
|
let mut relevant_pubkeys = event.relevant_pubkeys();
|
||||||
let referenced_event_ids = event.referenced_event_ids();
|
let referenced_event_ids = event.referenced_event_ids();
|
||||||
for referenced_event_id in referenced_event_ids {
|
for referenced_event_id in referenced_event_ids {
|
||||||
let pubkeys_relevant_to_referenced_event =
|
let pubkeys_relevant_to_referenced_event = self
|
||||||
self.pubkeys_subscribed_to_event_id(&referenced_event_id).await?;
|
.pubkeys_subscribed_to_event_id(&referenced_event_id)
|
||||||
|
.await?;
|
||||||
relevant_pubkeys.extend(pubkeys_relevant_to_referenced_event);
|
relevant_pubkeys.extend(pubkeys_relevant_to_referenced_event);
|
||||||
}
|
}
|
||||||
Ok(relevant_pubkeys)
|
Ok(relevant_pubkeys)
|
||||||
@ -411,7 +486,10 @@ impl NotificationManager {
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let user_device_tokens = self.get_user_device_tokens(pubkey).await?;
|
let user_device_tokens = self.get_user_device_tokens(pubkey).await?;
|
||||||
for device_token in user_device_tokens {
|
for device_token in user_device_tokens {
|
||||||
if !self.user_wants_notification(pubkey, device_token.clone(), event).await? {
|
if !self
|
||||||
|
.user_wants_notification(pubkey, device_token.clone(), event)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.send_event_notification_to_device_token(event, &device_token)
|
self.send_event_notification_to_device_token(event, &device_token)
|
||||||
@ -426,14 +504,20 @@ impl NotificationManager {
|
|||||||
device_token: String,
|
device_token: String,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
let notification_preferences = self.get_user_notification_settings(pubkey, device_token).await?;
|
let notification_preferences = self
|
||||||
|
.get_user_notification_settings(pubkey, device_token)
|
||||||
|
.await?;
|
||||||
if notification_preferences.only_notifications_from_following_enabled {
|
if notification_preferences.only_notifications_from_following_enabled {
|
||||||
if !self.nostr_network_helper.does_pubkey_follow_pubkey(pubkey, &event.author()).await {
|
if !self
|
||||||
|
.nostr_network_helper
|
||||||
|
.does_pubkey_follow_pubkey(pubkey, &event.author())
|
||||||
|
.await
|
||||||
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::TextNote => Ok(notification_preferences.mention_notifications_enabled), // TODO: Not 100% accurate
|
Kind::TextNote => Ok(notification_preferences.mention_notifications_enabled), // TODO: Not 100% accurate
|
||||||
Kind::EncryptedDirectMessage => Ok(notification_preferences.dm_notifications_enabled),
|
Kind::EncryptedDirectMessage => Ok(notification_preferences.dm_notifications_enabled),
|
||||||
Kind::Repost => Ok(notification_preferences.repost_notifications_enabled),
|
Kind::Repost => Ok(notification_preferences.repost_notifications_enabled),
|
||||||
Kind::GenericRepost => Ok(notification_preferences.repost_notifications_enabled),
|
Kind::GenericRepost => Ok(notification_preferences.repost_notifications_enabled),
|
||||||
@ -523,14 +607,20 @@ impl NotificationManager {
|
|||||||
.build(device_token, Default::default());
|
.build(device_token, Default::default());
|
||||||
|
|
||||||
payload.options.apns_topic = Some(self.apns_topic.as_str());
|
payload.options.apns_topic = Some(self.apns_topic.as_str());
|
||||||
payload.data.insert("nostr_event", serde_json::Value::String(event.try_as_json()?));
|
payload.data.insert(
|
||||||
|
"nostr_event",
|
||||||
|
serde_json::Value::String(event.try_as_json()?),
|
||||||
|
);
|
||||||
|
|
||||||
let apns_client_mutex_guard = self.apns_client.lock().await;
|
let apns_client_mutex_guard = self.apns_client.lock().await;
|
||||||
|
|
||||||
match apns_client_mutex_guard.send(payload).await {
|
match apns_client_mutex_guard.send(payload).await {
|
||||||
Ok(_response) => {},
|
Ok(_response) => {}
|
||||||
Err(e) => log::error!("Failed to send notification to device token '{}': {}", device_token, e),
|
Err(e) => log::error!(
|
||||||
|
"Failed to send notification to device token '{}': {}",
|
||||||
|
device_token,
|
||||||
|
e
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Notification sent to device token: {}", device_token);
|
log::info!("Notification sent to device token: {}", device_token);
|
||||||
@ -542,7 +632,10 @@ impl NotificationManager {
|
|||||||
// NOTE: This is simple because the client will handle formatting. These are just fallbacks.
|
// NOTE: This is simple because the client will handle formatting. These are just fallbacks.
|
||||||
let (title, body) = match event.kind {
|
let (title, body) = match event.kind {
|
||||||
nostr_sdk::Kind::TextNote => ("New activity".to_string(), event.content.clone()),
|
nostr_sdk::Kind::TextNote => ("New activity".to_string(), event.content.clone()),
|
||||||
nostr_sdk::Kind::EncryptedDirectMessage => ("New direct message".to_string(), "Contents are encrypted".to_string()),
|
nostr_sdk::Kind::EncryptedDirectMessage => (
|
||||||
|
"New direct message".to_string(),
|
||||||
|
"Contents are encrypted".to_string(),
|
||||||
|
),
|
||||||
nostr_sdk::Kind::Repost => ("Someone reposted".to_string(), event.content.clone()),
|
nostr_sdk::Kind::Repost => ("Someone reposted".to_string(), event.content.clone()),
|
||||||
nostr_sdk::Kind::Reaction => {
|
nostr_sdk::Kind::Reaction => {
|
||||||
let content_text = event.content.clone();
|
let content_text = event.content.clone();
|
||||||
@ -553,8 +646,11 @@ impl NotificationManager {
|
|||||||
_ => content_text.as_str(),
|
_ => content_text.as_str(),
|
||||||
};
|
};
|
||||||
("New reaction".to_string(), formatted_text.to_string())
|
("New reaction".to_string(), formatted_text.to_string())
|
||||||
},
|
}
|
||||||
nostr_sdk::Kind::ZapPrivateMessage => ("New zap private message".to_string(), "Contents are encrypted".to_string()),
|
nostr_sdk::Kind::ZapPrivateMessage => (
|
||||||
|
"New zap private message".to_string(),
|
||||||
|
"Contents are encrypted".to_string(),
|
||||||
|
),
|
||||||
nostr_sdk::Kind::ZapReceipt => ("Someone zapped you".to_string(), "".to_string()),
|
nostr_sdk::Kind::ZapReceipt => ("Someone zapped you".to_string(), "".to_string()),
|
||||||
_ => ("New activity".to_string(), "".to_string()),
|
_ => ("New activity".to_string(), "".to_string()),
|
||||||
};
|
};
|
||||||
@ -568,7 +664,10 @@ impl NotificationManager {
|
|||||||
pubkey: nostr::PublicKey,
|
pubkey: nostr::PublicKey,
|
||||||
device_token: &str,
|
device_token: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if self.is_pubkey_token_pair_registered(&pubkey, &device_token).await? {
|
if self
|
||||||
|
.is_pubkey_token_pair_registered(&pubkey, &device_token)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.save_user_device_info(pubkey, device_token).await
|
self.save_user_device_info(pubkey, device_token).await
|
||||||
@ -616,17 +715,16 @@ impl NotificationManager {
|
|||||||
let mut stmt = connection.prepare(
|
let mut stmt = connection.prepare(
|
||||||
"SELECT zap_notifications_enabled, mention_notifications_enabled, repost_notifications_enabled, reaction_notifications_enabled, dm_notifications_enabled, only_notifications_from_following_enabled FROM user_info WHERE pubkey = ? AND device_token = ?",
|
"SELECT zap_notifications_enabled, mention_notifications_enabled, repost_notifications_enabled, reaction_notifications_enabled, dm_notifications_enabled, only_notifications_from_following_enabled FROM user_info WHERE pubkey = ? AND device_token = ?",
|
||||||
)?;
|
)?;
|
||||||
let settings = stmt
|
let settings = stmt.query_row([pubkey.to_sql_string(), device_token], |row| {
|
||||||
.query_row([pubkey.to_sql_string(), device_token], |row| {
|
Ok(UserNotificationSettings {
|
||||||
Ok(UserNotificationSettings {
|
zap_notifications_enabled: row.get(0)?,
|
||||||
zap_notifications_enabled: row.get(0)?,
|
mention_notifications_enabled: row.get(1)?,
|
||||||
mention_notifications_enabled: row.get(1)?,
|
repost_notifications_enabled: row.get(2)?,
|
||||||
repost_notifications_enabled: row.get(2)?,
|
reaction_notifications_enabled: row.get(3)?,
|
||||||
reaction_notifications_enabled: row.get(3)?,
|
dm_notifications_enabled: row.get(4)?,
|
||||||
dm_notifications_enabled: row.get(4)?,
|
only_notifications_from_following_enabled: row.get(5)?,
|
||||||
only_notifications_from_following_enabled: row.get(5)?,
|
})
|
||||||
})
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
@ -663,7 +761,7 @@ pub struct UserNotificationSettings {
|
|||||||
repost_notifications_enabled: bool,
|
repost_notifications_enabled: bool,
|
||||||
reaction_notifications_enabled: bool,
|
reaction_notifications_enabled: bool,
|
||||||
dm_notifications_enabled: bool,
|
dm_notifications_enabled: bool,
|
||||||
only_notifications_from_following_enabled: bool
|
only_notifications_from_following_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NotificationStatus {
|
struct NotificationStatus {
|
||||||
|
@ -1,21 +1,15 @@
|
|||||||
|
use super::nostr_event_extensions::ExtendedEvent;
|
||||||
use nostr::nips::nip51::MuteList;
|
use nostr::nips::nip51::MuteList;
|
||||||
use nostr_sdk::Event;
|
use nostr_sdk::Event;
|
||||||
use super::nostr_event_extensions::ExtendedEvent;
|
|
||||||
|
|
||||||
|
pub fn should_mute_notification_for_mutelist(event: &Event, mute_list: &MuteList) -> bool {
|
||||||
pub fn should_mute_notification_for_mutelist(
|
|
||||||
event: &Event,
|
|
||||||
mute_list: &MuteList,
|
|
||||||
) -> bool {
|
|
||||||
for muted_public_key in &mute_list.public_keys {
|
for muted_public_key in &mute_list.public_keys {
|
||||||
if event.pubkey == *muted_public_key {
|
if event.pubkey == *muted_public_key {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for muted_event_id in &mute_list.event_ids {
|
for muted_event_id in &mute_list.event_ids {
|
||||||
if event.id == *muted_event_id
|
if event.id == *muted_event_id || event.referenced_event_ids().contains(&muted_event_id) {
|
||||||
|| event.referenced_event_ids().contains(&muted_event_id)
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,8 +104,13 @@ impl RelayConnection {
|
|||||||
ClientMessage::Event(event) => {
|
ClientMessage::Event(event) => {
|
||||||
log::info!("Received event with id: {:?}", event.id.to_hex());
|
log::info!("Received event with id: {:?}", event.id.to_hex());
|
||||||
log::debug!("Event received: {:?}", event);
|
log::debug!("Event received: {:?}", event);
|
||||||
self.notification_manager.event_saver.save_if_needed(&event).await?;
|
self.notification_manager
|
||||||
self.notification_manager.send_notifications_if_needed(&event).await?;
|
.event_saver
|
||||||
|
.save_if_needed(&event)
|
||||||
|
.await?;
|
||||||
|
self.notification_manager
|
||||||
|
.send_notifications_if_needed(&event)
|
||||||
|
.await?;
|
||||||
let notice_message = format!("blocked: This relay does not store events");
|
let notice_message = format!("blocked: This relay does not store events");
|
||||||
let response = RelayMessage::Ok {
|
let response = RelayMessage::Ok {
|
||||||
event_id: event.id,
|
event_id: event.id,
|
||||||
|
Reference in New Issue
Block a user