mirror of
https://github.com/nostrlabs-io/notepush.git
synced 2025-06-14 11:07:43 +00:00
Push notification preferences
This commit implements basic push notification preferences as well as the interface to change them. Furthermore, the API interface was reworked to follow better REST API conventions. Testing -------- PASS Device: iPhone 15 simulators iOS: 17.5 Damus: 4ea6c360e6e33747cb09ecf085049948ec1dadd1 (A commit from GH issue #2360) notepush: This commit Steps: 1. Disable all types of notifications, except for DMs. 2. Send a like to this user's post. Push notification should not appear. PASS 3. Send a DM to this user's post. Push notification should appear. PASS Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1562,9 +1562,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::nip98_auth;
|
||||
use crate::notification_manager::notification_manager::UserNotificationSettings;
|
||||
use crate::relay_connection::RelayConnection;
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Buf;
|
||||
@ -9,50 +10,21 @@ use hyper_tungstenite;
|
||||
|
||||
use http_body_util::BodyExt;
|
||||
use nostr;
|
||||
use serde_json::from_value;
|
||||
|
||||
use crate::notification_manager::NotificationManager;
|
||||
use hyper::Method;
|
||||
use log;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
struct ParsedRequest {
|
||||
uri: String,
|
||||
method: Method,
|
||||
body_bytes: Option<Vec<u8>>,
|
||||
authorized_pubkey: nostr::PublicKey,
|
||||
}
|
||||
|
||||
impl ParsedRequest {
|
||||
fn body_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
if let Some(body_bytes) = &self.body_bytes {
|
||||
Ok(serde_json::from_slice(body_bytes)?)
|
||||
} else {
|
||||
Ok(json!({}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct APIResponse {
|
||||
status: StatusCode,
|
||||
body: Value,
|
||||
}
|
||||
|
||||
pub struct APIHandler {
|
||||
notification_manager: Arc<NotificationManager>,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl Clone for APIHandler {
|
||||
fn clone(&self) -> Self {
|
||||
APIHandler {
|
||||
notification_manager: self.notification_manager.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl APIHandler {
|
||||
pub fn new(notification_manager: Arc<NotificationManager>, base_url: String) -> Self {
|
||||
APIHandler {
|
||||
@ -60,6 +32,8 @@ impl APIHandler {
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HTTP handling
|
||||
|
||||
pub async fn handle_http_request(
|
||||
&self,
|
||||
@ -185,22 +159,37 @@ impl APIHandler {
|
||||
authorized_pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Router
|
||||
|
||||
async fn handle_parsed_http_request(
|
||||
&self,
|
||||
parsed_request: &ParsedRequest,
|
||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||
match (&parsed_request.method, parsed_request.uri.as_str()) {
|
||||
(&Method::POST, "/user-info") => self.handle_user_info(parsed_request).await,
|
||||
(&Method::POST, "/user-info/remove") => {
|
||||
self.handle_user_info_remove(parsed_request).await
|
||||
}
|
||||
_ => Ok(APIResponse {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
body: json!({ "error": "Not found" }),
|
||||
}),
|
||||
|
||||
if let Some(url_params) = route_match(&Method::PUT, "/user-info/:pubkey/:deviceToken", &parsed_request) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return self.set_user_settings(parsed_request, &url_params).await;
|
||||
}
|
||||
|
||||
Ok(APIResponse {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
body: json!({ "error": "Not found" }),
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
async fn authenticate(
|
||||
&self,
|
||||
@ -220,51 +209,283 @@ impl APIHandler {
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
// MARK: - Endpoint handlers
|
||||
|
||||
async fn handle_user_info(
|
||||
&self,
|
||||
req: &ParsedRequest,
|
||||
url_params: &HashMap<&str, String>,
|
||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||
let body = req.body_json()?;
|
||||
|
||||
if let Some(device_token) = body["deviceToken"].as_str() {
|
||||
self.notification_manager.save_user_device_info(req.authorized_pubkey, device_token).await?;
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!({ "message": "User info saved successfully" }),
|
||||
});
|
||||
} else {
|
||||
return Ok(APIResponse {
|
||||
// Early return if `deviceToken` is missing
|
||||
let device_token = match url_params.get("deviceToken") {
|
||||
Some(token) => token,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "deviceToken is required" }),
|
||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` is missing
|
||||
let pubkey = match url_params.get("pubkey") {
|
||||
Some(key) => key,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "pubkey is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Validate the `pubkey` and prepare it for use
|
||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "Invalid pubkey" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||
if pubkey != req.authorized_pubkey {
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: json!({ "error": "Forbidden" }),
|
||||
});
|
||||
}
|
||||
|
||||
// Proceed with the main logic after passing all checks
|
||||
self.notification_manager.save_user_device_info(pubkey, device_token).await?;
|
||||
Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!({ "message": "User info saved successfully" }),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_user_info_remove(
|
||||
&self,
|
||||
req: &ParsedRequest,
|
||||
url_params: &HashMap<&str, String>,
|
||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||
let body: Value = req.body_json()?;
|
||||
|
||||
if let Some(device_token) = body["deviceToken"].as_str() {
|
||||
self.notification_manager.remove_user_device_info(req.authorized_pubkey, device_token).await?;
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!({ "message": "User info removed successfully" }),
|
||||
});
|
||||
} else {
|
||||
return Ok(APIResponse {
|
||||
// Early return if `deviceToken` is missing
|
||||
let device_token = match url_params.get("deviceToken") {
|
||||
Some(token) => token,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "deviceToken is required" }),
|
||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` is missing
|
||||
let pubkey = match url_params.get("pubkey") {
|
||||
Some(key) => key,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "pubkey is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Validate the `pubkey` and prepare it for use
|
||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "Invalid pubkey" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||
if pubkey != req.authorized_pubkey {
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: json!({ "error": "Forbidden" }),
|
||||
});
|
||||
}
|
||||
|
||||
// Proceed with the main logic after passing all checks
|
||||
self.notification_manager.remove_user_device_info(pubkey, device_token).await?;
|
||||
|
||||
Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!({ "message": "User info removed successfully" }),
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_user_settings(
|
||||
&self,
|
||||
req: &ParsedRequest,
|
||||
url_params: &HashMap<&str, String>,
|
||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||
// Early return if `deviceToken` is missing
|
||||
let device_token = match url_params.get("deviceToken") {
|
||||
Some(token) => token,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` is missing
|
||||
let pubkey = match url_params.get("pubkey") {
|
||||
Some(key) => key,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "pubkey is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Validate the `pubkey` and prepare it for use
|
||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "Invalid pubkey" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||
if pubkey != req.authorized_pubkey {
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: json!({ "error": "Forbidden" }),
|
||||
});
|
||||
}
|
||||
|
||||
// Proceed with the main logic after passing all checks
|
||||
let body = req.body_json()?;
|
||||
|
||||
let settings: UserNotificationSettings = match from_value(body.clone()) {
|
||||
Ok(settings) => settings,
|
||||
Err(_) => {
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "Invalid settings" }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.notification_manager.save_user_notification_settings(&req.authorized_pubkey, device_token.to_string(), settings).await?;
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!({ "message": "User settings saved successfully" }),
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_user_settings(
|
||||
&self,
|
||||
req: &ParsedRequest,
|
||||
url_params: &HashMap<&str, String>,
|
||||
) -> Result<APIResponse, Box<dyn std::error::Error>> {
|
||||
// Early return if `deviceToken` is missing
|
||||
let device_token = match url_params.get("deviceToken") {
|
||||
Some(token) => token,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "deviceToken is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` is missing
|
||||
let pubkey = match url_params.get("pubkey") {
|
||||
Some(key) => key,
|
||||
None => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "pubkey is required on the URL" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Validate the `pubkey` and prepare it for use
|
||||
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Ok(APIResponse {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
body: json!({ "error": "Invalid pubkey" }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Early return if `pubkey` does not match `req.authorized_pubkey`
|
||||
if pubkey != req.authorized_pubkey {
|
||||
return Ok(APIResponse {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: json!({ "error": "Forbidden" }),
|
||||
});
|
||||
}
|
||||
|
||||
// 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?;
|
||||
|
||||
Ok(APIResponse {
|
||||
status: StatusCode::OK,
|
||||
body: json!(settings),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
impl Clone for APIHandler {
|
||||
fn clone(&self) -> Self {
|
||||
APIHandler {
|
||||
notification_manager: self.notification_manager.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper types
|
||||
|
||||
// Define enum error types including authentication error
|
||||
#[derive(Debug, Error)]
|
||||
enum APIError {
|
||||
#[error("Authentication error: {0}")]
|
||||
AuthenticationError(String),
|
||||
}
|
||||
|
||||
struct ParsedRequest {
|
||||
uri: String,
|
||||
method: Method,
|
||||
body_bytes: Option<Vec<u8>>,
|
||||
authorized_pubkey: nostr::PublicKey,
|
||||
}
|
||||
|
||||
impl ParsedRequest {
|
||||
fn body_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
if let Some(body_bytes) = &self.body_bytes {
|
||||
Ok(serde_json::from_slice(body_bytes)?)
|
||||
} else {
|
||||
Ok(json!({}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct APIResponse {
|
||||
status: StatusCode,
|
||||
body: Value,
|
||||
}
|
||||
|
||||
// MARK: - Helper functions
|
||||
|
||||
/// 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" }
|
||||
fn route_match<'a>(method: &Method, path: &'a str, req: &ParsedRequest) -> Option<HashMap<&'a str, String>> {
|
||||
if method != req.method {
|
||||
return None;
|
||||
}
|
||||
let mut params = HashMap::new();
|
||||
let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||
let req_segments: Vec<&str> = req.uri.split('/').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
if path_segments.len() != req_segments.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (i, segment) in path_segments.iter().enumerate() {
|
||||
if segment.starts_with(':') {
|
||||
let key = &segment[1..];
|
||||
let value = req_segments[i].to_string();
|
||||
params.insert(key, value);
|
||||
} else if segment != &req_segments[i] {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(params)
|
||||
}
|
||||
|
@ -4,8 +4,11 @@ use nostr::event::EventId;
|
||||
use nostr::key::PublicKey;
|
||||
use nostr::types::Timestamp;
|
||||
use nostr_sdk::JsonUtil;
|
||||
use nostr_sdk::Kind;
|
||||
use rusqlite;
|
||||
use rusqlite::params;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::Mutex;
|
||||
use std::collections::HashSet;
|
||||
use tokio;
|
||||
@ -65,6 +68,8 @@ impl NotificationManager {
|
||||
// MARK: - Database setup operations
|
||||
|
||||
pub fn setup_database(db: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
// Initial schema setup
|
||||
|
||||
db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
@ -94,8 +99,17 @@ impl NotificationManager {
|
||||
[],
|
||||
)?;
|
||||
|
||||
Self::add_column_if_not_exists(&db, "notifications", "sent_at", "INTEGER")?;
|
||||
Self::add_column_if_not_exists(&db, "user_info", "added_at", "INTEGER")?;
|
||||
Self::add_column_if_not_exists(&db, "notifications", "sent_at", "INTEGER", None)?;
|
||||
Self::add_column_if_not_exists(&db, "user_info", "added_at", "INTEGER", None)?;
|
||||
|
||||
// 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(&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"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -105,6 +119,7 @@ impl NotificationManager {
|
||||
table_name: &str,
|
||||
column_name: &str,
|
||||
column_type: &str,
|
||||
default_value: Option<&str>,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
let query = format!("PRAGMA table_info({})", table_name);
|
||||
let mut stmt = db.prepare(&query)?;
|
||||
@ -115,8 +130,11 @@ impl NotificationManager {
|
||||
|
||||
if !column_names.contains(&column_name.to_string()) {
|
||||
let query = format!(
|
||||
"ALTER TABLE {} ADD COLUMN {} {}",
|
||||
table_name, column_name, column_type
|
||||
"ALTER TABLE {} ADD COLUMN {} {} {}",
|
||||
table_name, column_name, column_type, match default_value {
|
||||
Some(value) => format!("DEFAULT {}", value),
|
||||
None => "".to_string(),
|
||||
},
|
||||
);
|
||||
db.execute(&query, [])?;
|
||||
}
|
||||
@ -251,11 +269,34 @@ impl NotificationManager {
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let user_device_tokens = self.get_user_device_tokens(pubkey).await?;
|
||||
for device_token in user_device_tokens {
|
||||
if !self.user_wants_notification(pubkey, device_token.clone(), event).await? {
|
||||
continue;
|
||||
}
|
||||
self.send_event_notification_to_device_token(event, &device_token)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn user_wants_notification(
|
||||
&self,
|
||||
pubkey: &PublicKey,
|
||||
device_token: String,
|
||||
event: &Event,
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let notification_preferences = self.get_user_notification_settings(pubkey, device_token).await?;
|
||||
match event.kind {
|
||||
Kind::TextNote => Ok(notification_preferences.mention_notifications_enabled), // TODO: Not 100% accurate
|
||||
Kind::EncryptedDirectMessage => Ok(notification_preferences.dm_notifications_enabled),
|
||||
Kind::Repost => Ok(notification_preferences.repost_notifications_enabled),
|
||||
Kind::GenericRepost => Ok(notification_preferences.repost_notifications_enabled),
|
||||
Kind::Reaction => Ok(notification_preferences.reaction_notifications_enabled),
|
||||
Kind::ZapPrivateMessage => Ok(notification_preferences.zap_notifications_enabled),
|
||||
Kind::ZapRequest => Ok(notification_preferences.zap_notifications_enabled),
|
||||
Kind::ZapReceipt => Ok(notification_preferences.zap_notifications_enabled),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_device_tokens(
|
||||
&self,
|
||||
@ -343,6 +384,8 @@ impl NotificationManager {
|
||||
};
|
||||
(title, "".to_string(), body)
|
||||
}
|
||||
|
||||
// MARK: - User device info and settings
|
||||
|
||||
pub async fn save_user_device_info(
|
||||
&self,
|
||||
@ -375,6 +418,65 @@ impl NotificationManager {
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user_notification_settings(
|
||||
&self,
|
||||
pubkey: &PublicKey,
|
||||
device_token: String,
|
||||
) -> Result<UserNotificationSettings, Box<dyn std::error::Error>> {
|
||||
let db_mutex_guard = self.db.lock().await;
|
||||
let connection = db_mutex_guard.get()?;
|
||||
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 = ?",
|
||||
)?;
|
||||
let settings = stmt
|
||||
.query_row([pubkey.to_sql_string(), device_token], |row| {
|
||||
Ok(UserNotificationSettings {
|
||||
zap_notifications_enabled: row.get(0)?,
|
||||
mention_notifications_enabled: row.get(1)?,
|
||||
repost_notifications_enabled: row.get(2)?,
|
||||
reaction_notifications_enabled: row.get(3)?,
|
||||
dm_notifications_enabled: row.get(4)?,
|
||||
only_notifications_from_following_enabled: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub async fn save_user_notification_settings(
|
||||
&self,
|
||||
pubkey: &PublicKey,
|
||||
device_token: String,
|
||||
settings: UserNotificationSettings,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db_mutex_guard = self.db.lock().await;
|
||||
let connection = db_mutex_guard.get()?;
|
||||
connection.execute(
|
||||
"UPDATE user_info SET zap_notifications_enabled = ?, mention_notifications_enabled = ?, repost_notifications_enabled = ?, reaction_notifications_enabled = ?, dm_notifications_enabled = ?, only_notifications_from_following_enabled = ? WHERE pubkey = ? AND device_token = ?",
|
||||
params![
|
||||
settings.zap_notifications_enabled,
|
||||
settings.mention_notifications_enabled,
|
||||
settings.repost_notifications_enabled,
|
||||
settings.reaction_notifications_enabled,
|
||||
settings.dm_notifications_enabled,
|
||||
settings.only_notifications_from_following_enabled,
|
||||
pubkey.to_sql_string(),
|
||||
device_token,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct UserNotificationSettings {
|
||||
zap_notifications_enabled: bool,
|
||||
mention_notifications_enabled: bool,
|
||||
repost_notifications_enabled: bool,
|
||||
reaction_notifications_enabled: bool,
|
||||
dm_notifications_enabled: bool,
|
||||
only_notifications_from_following_enabled: bool
|
||||
}
|
||||
|
||||
struct NotificationStatus {
|
||||
|
Reference in New Issue
Block a user