rewrite subscription complete
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1955,6 +1955,7 @@ name = "ntfy-daemon"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-channel",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"capnp 0.18.13",
|
"capnp 0.18.13",
|
||||||
"capnp-rpc",
|
"capnp-rpc",
|
||||||
|
|||||||
@ -31,3 +31,4 @@ regex = "1.9.6"
|
|||||||
oo7 = "0.2.1"
|
oo7 = "0.2.1"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
http = "1.1.0"
|
http = "1.1.0"
|
||||||
|
async-channel = "2.3.1"
|
||||||
@ -1,6 +1,7 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
@ -135,14 +136,14 @@ pub struct Credential {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
keyring: Rc<dyn LightKeyring>,
|
keyring: Arc<dyn LightKeyring + Send + Sync>,
|
||||||
creds: Rc<RefCell<HashMap<String, Credential>>>,
|
creds: Arc<RwLock<HashMap<String, Credential>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
pub async fn new() -> anyhow::Result<Self> {
|
pub async fn new() -> anyhow::Result<Self> {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
keyring: Rc::new(RealKeyring {
|
keyring: Arc::new(RealKeyring {
|
||||||
keyring: oo7::Keyring::new()
|
keyring: oo7::Keyring::new()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to start Secret Service"),
|
.expect("Failed to start Secret Service"),
|
||||||
@ -154,7 +155,7 @@ impl Credentials {
|
|||||||
}
|
}
|
||||||
pub async fn new_nullable(credentials: Vec<Credential>) -> anyhow::Result<Self> {
|
pub async fn new_nullable(credentials: Vec<Credential>) -> anyhow::Result<Self> {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
keyring: Rc::new(NullableKeyring::with_credentials(credentials)),
|
keyring: Arc::new(NullableKeyring::with_credentials(credentials)),
|
||||||
creds: Default::default(),
|
creds: Default::default(),
|
||||||
};
|
};
|
||||||
this.load().await?;
|
this.load().await?;
|
||||||
@ -168,12 +169,13 @@ impl Credentials {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
||||||
|
|
||||||
self.creds.borrow_mut().clear();
|
let mut lock = self.creds.write().unwrap();
|
||||||
|
lock.clear();
|
||||||
for item in values {
|
for item in values {
|
||||||
let attrs = item
|
let attrs = item
|
||||||
.attributes()
|
.attributes()
|
||||||
.await;
|
.await;
|
||||||
self.creds.borrow_mut().insert(
|
lock.insert(
|
||||||
attrs["server"].to_string(),
|
attrs["server"].to_string(),
|
||||||
Credential {
|
Credential {
|
||||||
username: attrs["username"].to_string(),
|
username: attrs["username"].to_string(),
|
||||||
@ -184,14 +186,14 @@ impl Credentials {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn get(&self, server: &str) -> Option<Credential> {
|
pub fn get(&self, server: &str) -> Option<Credential> {
|
||||||
self.creds.borrow().get(server).cloned()
|
self.creds.read().unwrap().get(server).cloned()
|
||||||
}
|
}
|
||||||
pub fn list_all(&self) -> HashMap<String, Credential> {
|
pub fn list_all(&self) -> HashMap<String, Credential> {
|
||||||
self.creds.borrow().clone()
|
self.creds.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
pub async fn insert(&self, server: &str, username: &str, password: &str) -> anyhow::Result<()> {
|
pub async fn insert(&self, server: &str, username: &str, password: &str) -> anyhow::Result<()> {
|
||||||
{
|
{
|
||||||
if let Some(cred) = self.creds.borrow().get(server) {
|
if let Some(cred) = self.creds.read().unwrap().get(server) {
|
||||||
if cred.username != username {
|
if cred.username != username {
|
||||||
anyhow::bail!("You can add only one account per server");
|
anyhow::bail!("You can add only one account per server");
|
||||||
}
|
}
|
||||||
@ -207,7 +209,7 @@ impl Credentials {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
||||||
|
|
||||||
self.creds.borrow_mut().insert(
|
self.creds.write().unwrap().insert(
|
||||||
server.to_string(),
|
server.to_string(),
|
||||||
Credential {
|
Credential {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
@ -219,7 +221,8 @@ impl Credentials {
|
|||||||
pub async fn delete(&self, server: &str) -> anyhow::Result<()> {
|
pub async fn delete(&self, server: &str) -> anyhow::Result<()> {
|
||||||
let creds = {
|
let creds = {
|
||||||
self.creds
|
self.creds
|
||||||
.borrow()
|
.read()
|
||||||
|
.unwrap()
|
||||||
.get(server)
|
.get(server)
|
||||||
.ok_or(anyhow::anyhow!("server creds not found"))?
|
.ok_or(anyhow::anyhow!("server creds not found"))?
|
||||||
.clone()
|
.clone()
|
||||||
@ -234,7 +237,8 @@ impl Credentials {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
.map_err(|e| capnp::Error::failed(e.to_string()))?;
|
||||||
self.creds
|
self.creds
|
||||||
.borrow_mut()
|
.write()
|
||||||
|
.unwrap()
|
||||||
.remove(server)
|
.remove(server)
|
||||||
.ok_or(anyhow::anyhow!("server creds not found"))?;
|
.ok_or(anyhow::anyhow!("server creds not found"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
|||||||
use reqwest::{header::HeaderMap, Client, Request, RequestBuilder, Response, ResponseBuilderExt};
|
use reqwest::{header::HeaderMap, Client, Request, RequestBuilder, Response, ResponseBuilderExt};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@ -86,26 +86,90 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct NullableClient {
|
pub struct NullableClient {
|
||||||
responses: Arc<RwLock<HashMap<String, Response>>>,
|
responses: Arc<RwLock<HashMap<String, VecDeque<Response>>>>,
|
||||||
default_response: Arc<RwLock<Option<Box<dyn Fn() -> Response + Send + Sync + 'static>>>>,
|
default_response: Arc<RwLock<Option<Box<dyn Fn() -> Response + Send + Sync + 'static>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NullableClient {
|
/// Builder for configuring NullableClient
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NullableClientBuilder {
|
||||||
|
responses: HashMap<String, VecDeque<Response>>,
|
||||||
|
default_response: Option<Box<dyn Fn() -> Response + Send + Sync + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NullableClientBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_response(&self, url: &str, response: Response) {
|
/// Add a single response for a specific URL
|
||||||
|
pub fn response(mut self, url: impl Into<String>, response: Response) -> Self {
|
||||||
self.responses
|
self.responses
|
||||||
.write()
|
.entry(url.into())
|
||||||
.await
|
.or_default()
|
||||||
.insert(url.to_string(), response);
|
.push_back(response);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_default_response(&self, res: Box<dyn Fn() -> Response + Send + Sync + 'static>) {
|
/// Add multiple responses for a specific URL that will be returned in sequence
|
||||||
*self.default_response.write().await = Some(res);
|
pub fn responses(mut self, url: impl Into<String>, responses: Vec<Response>) -> Self {
|
||||||
|
self.responses.insert(url.into(), responses.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a default response generator for any unmatched URLs
|
||||||
|
pub fn default_response(
|
||||||
|
mut self,
|
||||||
|
response: impl Fn() -> Response + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.default_response = Some(Box::new(response));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to quickly add a JSON response
|
||||||
|
pub fn json_response(
|
||||||
|
self,
|
||||||
|
url: impl Into<String>,
|
||||||
|
status: u16,
|
||||||
|
body: impl serde::Serialize,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let response = http::response::Builder::new()
|
||||||
|
.status(status)
|
||||||
|
.body(serde_json::to_string(&body)?)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
Ok(self.response(url, response))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to quickly add a text response
|
||||||
|
pub fn text_response(
|
||||||
|
self,
|
||||||
|
url: impl Into<String>,
|
||||||
|
status: u16,
|
||||||
|
body: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
let response = http::response::Builder::new()
|
||||||
|
.status(status)
|
||||||
|
.body(body.into())
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
self.response(url, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> NullableClient {
|
||||||
|
NullableClient {
|
||||||
|
responses: Arc::new(RwLock::new(self.responses.into_iter().map(|(k, v)| (k, v.into())).collect())),
|
||||||
|
default_response: Arc::new(RwLock::new(self.default_response)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NullableClient {
|
||||||
|
pub fn builder() -> NullableClientBuilder {
|
||||||
|
NullableClientBuilder::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,15 +180,28 @@ impl LightHttpClient for NullableClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, request: Request) -> Result<Response> {
|
async fn execute(&self, request: Request) -> Result<Response> {
|
||||||
time::sleep(Duration::from_millis(1)).await; // else we spam the thread with responses
|
time::sleep(Duration::from_millis(1)).await;
|
||||||
// Get the configured response or return a default one
|
|
||||||
let url = request.url().to_string();
|
let url = request.url().to_string();
|
||||||
if let Some(response) = self.responses.write().await.remove(&url) {
|
let mut responses = self.responses.write().await;
|
||||||
Ok(response)
|
|
||||||
} else if let Some(res) = &*self.default_response.read().await {
|
if let Some(url_responses) = responses.get_mut(&url) {
|
||||||
Ok(res())
|
if let Some(response) = url_responses.pop_front() {
|
||||||
|
// Remove the URL entry if no more responses
|
||||||
|
if url_responses.is_empty() {
|
||||||
|
responses.remove(&url);
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
if let Some(default_fn) = &*self.default_response.read().await {
|
||||||
|
Ok(default_fn())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("no response configured for URL: {}", url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(default_fn) = &*self.default_response.read().await {
|
||||||
|
Ok(default_fn())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow::anyhow!("no response"))
|
Err(anyhow::anyhow!("no response configured for URL: {}", url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,80 +209,93 @@ impl LightHttpClient for NullableClient {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_nullable() -> Result<()> {
|
async fn test_nullable_with_builder() -> Result<()> {
|
||||||
let client = NullableClient::new();
|
// Configure client using builder pattern
|
||||||
|
let client = NullableClient::builder()
|
||||||
|
.text_response("https://api.example.com/topic", 200, "ok")
|
||||||
|
.json_response(
|
||||||
|
"https://api.example.com/json",
|
||||||
|
200,
|
||||||
|
json!({ "status": "success" }),
|
||||||
|
)?
|
||||||
|
.default_response(|| {
|
||||||
|
http::response::Builder::new()
|
||||||
|
.status(404)
|
||||||
|
.body("not found")
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
// Configure mock response
|
let http_client = HttpClient::new_nullable(client);
|
||||||
let mock_response = http::response::Builder::new()
|
let request_tracker = http_client.request_tracker().await;
|
||||||
.status(200)
|
|
||||||
.body("ok")
|
|
||||||
.unwrap()
|
|
||||||
.into();
|
|
||||||
client
|
|
||||||
.set_response("https://api.example.com/topic", mock_response)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let client = HttpClient::new_nullable(client);
|
|
||||||
let request_tracker = client.request_tracker().await;
|
|
||||||
|
|
||||||
let req = client
|
|
||||||
.get("https://api.example.com/topic")
|
|
||||||
.header("Content-Type", "application/x-ndjson")
|
|
||||||
.header("Transfer-Encoding", "chunked")
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Execute request
|
|
||||||
let response = client.execute(req).await?;
|
|
||||||
|
|
||||||
|
// Test successful text response
|
||||||
|
let request = http_client.get("https://api.example.com/topic").build()?;
|
||||||
|
let response = http_client.execute(request).await?;
|
||||||
assert_eq!(response.status(), 200);
|
assert_eq!(response.status(), 200);
|
||||||
assert_eq!(response.bytes().await.unwrap(), b"ok"[..]);
|
assert_eq!(response.text().await?, "ok");
|
||||||
|
|
||||||
|
// Test successful JSON response
|
||||||
|
let request = http_client.get("https://api.example.com/json").build()?;
|
||||||
|
let response = http_client.execute(request).await?;
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
assert_eq!(response.text().await?, r#"{"status":"success"}"#);
|
||||||
|
|
||||||
|
// Test default response
|
||||||
|
let request = http_client.get("https://api.example.com/unknown").build()?;
|
||||||
|
let response = http_client.execute(request).await?;
|
||||||
|
assert_eq!(response.status(), 404);
|
||||||
|
assert_eq!(response.text().await?, "not found");
|
||||||
|
|
||||||
// Verify recorded requests
|
// Verify recorded requests
|
||||||
let requests = request_tracker.items().await;
|
let requests = request_tracker.items().await;
|
||||||
assert_eq!(requests.len(), 1);
|
assert_eq!(requests.len(), 3);
|
||||||
|
|
||||||
let request = &requests[0];
|
|
||||||
assert_eq!(request.method, "GET");
|
|
||||||
assert_eq!(
|
|
||||||
request.headers.get("Content-Type").unwrap(),
|
|
||||||
"application/x-ndjson"
|
|
||||||
);
|
|
||||||
assert_eq!(request.headers.get("Transfer-Encoding").unwrap(), "chunked");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_nullable_with_failing_response() -> Result<()> {
|
async fn test_sequence_of_responses() -> Result<()> {
|
||||||
let client = NullableClient::new();
|
// Configure client with multiple responses for the same URL
|
||||||
|
let client = NullableClient::builder()
|
||||||
|
.responses(
|
||||||
|
"https://api.example.com/sequence",
|
||||||
|
vec![
|
||||||
|
http::response::Builder::new()
|
||||||
|
.status(200)
|
||||||
|
.body("first")
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
http::response::Builder::new()
|
||||||
|
.status(200)
|
||||||
|
.body("second")
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
// Configure mock response
|
let http_client = HttpClient::new_nullable(client);
|
||||||
let mock_response = http::response::Builder::new()
|
|
||||||
.status(400)
|
|
||||||
.body("fail")
|
|
||||||
.unwrap()
|
|
||||||
.into();
|
|
||||||
client
|
|
||||||
.set_response("https://api.example.com/topic", mock_response)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let req = client
|
// First request gets first response
|
||||||
.get("https://api.example.com/topic")
|
let request = http_client.get("https://api.example.com/sequence").build()?;
|
||||||
.header("Content-Type", "application/x-ndjson")
|
let response = http_client.execute(request).await?;
|
||||||
.header("Transfer-Encoding", "chunked")
|
assert_eq!(response.text().await?, "first");
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Execute request
|
// Second request gets second response
|
||||||
let response = client.execute(req).await?;
|
let request = http_client.get("https://api.example.com/sequence").build()?;
|
||||||
let response: Result<_, _> = response.error_for_status();
|
let response = http_client.execute(request).await?;
|
||||||
|
assert_eq!(response.text().await?, "second");
|
||||||
|
|
||||||
dbg!(&response);
|
// Third request fails (no more responses)
|
||||||
assert!(matches!(response, Err(_)));
|
let request = http_client.get("https://api.example.com/sequence").build()?;
|
||||||
|
let result = http_client.execute(request).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,8 +8,12 @@ mod http_client;
|
|||||||
mod output_tracker;
|
mod output_tracker;
|
||||||
mod listener;
|
mod listener;
|
||||||
mod ntfy;
|
mod ntfy;
|
||||||
|
mod subscription;
|
||||||
|
|
||||||
pub use ntfy::Ntfy;
|
pub use subscription::SubscriptionHandle;
|
||||||
|
pub use listener::*;
|
||||||
|
pub use ntfy::NtfyHandle;
|
||||||
|
pub use ntfy::start;
|
||||||
|
|
||||||
pub mod ntfy_capnp {
|
pub mod ntfy_capnp {
|
||||||
include!(concat!(env!("OUT_DIR"), "/src/ntfy_capnp.rs"));
|
include!(concat!(env!("OUT_DIR"), "/src/ntfy_capnp.rs"));
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
use std::{rc::Rc, time::Duration};
|
use std::{time::Duration};
|
||||||
|
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::AsyncBufReadExt;
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tokio::task::{self, spawn_local, AbortHandle, LocalSet};
|
use tokio::task::{self, spawn_local, AbortHandle, LocalSet};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
select,
|
||||||
sync::{broadcast, mpsc, watch},
|
sync::{mpsc, watch, oneshot},
|
||||||
};
|
};
|
||||||
use tokio_stream::wrappers::LinesStream;
|
use tokio_stream::wrappers::LinesStream;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
@ -23,7 +25,7 @@ use tokio::time::timeout;
|
|||||||
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(240); // 4 minutes
|
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(240); // 4 minutes
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "event")]
|
#[serde(tag = "event")]
|
||||||
pub enum ServerEvent {
|
pub enum ServerEvent {
|
||||||
#[serde(rename = "open")]
|
#[serde(rename = "open")]
|
||||||
@ -34,12 +36,9 @@ pub enum ServerEvent {
|
|||||||
topic: String,
|
topic: String,
|
||||||
},
|
},
|
||||||
#[serde(rename = "message")]
|
#[serde(rename = "message")]
|
||||||
Message {
|
Message (
|
||||||
id: String,
|
models::Message,
|
||||||
expires: Option<usize>,
|
),
|
||||||
#[serde(flatten)]
|
|
||||||
message: models::Message,
|
|
||||||
},
|
|
||||||
#[serde(rename = "keepalive")]
|
#[serde(rename = "keepalive")]
|
||||||
KeepAlive {
|
KeepAlive {
|
||||||
id: String,
|
id: String,
|
||||||
@ -64,10 +63,11 @@ pub struct ListenerConfig {
|
|||||||
pub(crate) since: u64,
|
pub(crate) since: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ListenerCommand {
|
pub enum ListenerCommand {
|
||||||
Restart,
|
Restart,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
GetState(oneshot::Sender<ConnectionState>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn topic_request(
|
fn topic_request(
|
||||||
@ -108,63 +108,82 @@ pub enum ConnectionState {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConnectionHandler {
|
pub struct ListenerActor {
|
||||||
pub event_tx: watch::Sender<ListenerEvent>,
|
pub event_tx: async_channel::Sender<ListenerEvent>,
|
||||||
pub commands_rx: Option<broadcast::Receiver<ListenerCommand>>,
|
pub commands_rx: Option<mpsc::Receiver<ListenerCommand>>,
|
||||||
pub config: ListenerConfig,
|
pub config: ListenerConfig,
|
||||||
pub state: Rc<RefCell<ConnectionState>>,
|
pub state: ConnectionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionHandler {
|
impl ListenerActor {
|
||||||
fn new(
|
pub fn new(
|
||||||
config: ListenerConfig,
|
config: ListenerConfig,
|
||||||
event_tx: watch::Sender<ListenerEvent>,
|
) -> ListenerHandle {
|
||||||
commands_rx: broadcast::Receiver<ListenerCommand>,
|
let (event_tx, event_rx) = async_channel::bounded(64);
|
||||||
) -> Self {
|
let (commands_tx, commands_rx) = mpsc::channel(1);
|
||||||
let this = Self {
|
|
||||||
event_tx,
|
let config_clone = config.clone();
|
||||||
commands_rx: Some(commands_rx),
|
|
||||||
|
// use a new local set to isolate panics
|
||||||
|
let local_set = LocalSet::new();
|
||||||
|
local_set.spawn_local(async move {
|
||||||
|
|
||||||
|
let this = Self {
|
||||||
|
event_tx,
|
||||||
|
commands_rx: Some(commands_rx),
|
||||||
|
config: config_clone,
|
||||||
|
state: ConnectionState::Unitialized,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.run_loop().await;
|
||||||
|
});
|
||||||
|
spawn_local(local_set);
|
||||||
|
|
||||||
|
ListenerHandle {
|
||||||
|
events: event_rx,
|
||||||
config,
|
config,
|
||||||
state: Rc::new(RefCell::new(ConnectionState::Unitialized)),
|
commands: commands_tx,
|
||||||
};
|
listener_actor: Arc::new(RwLock::new(None)),
|
||||||
this
|
join_handle: Arc::new(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut self) -> task::JoinHandle<()> {
|
pub async fn run_loop(mut self) {
|
||||||
spawn_local(async move {
|
|
||||||
let mut commands_rx = self.commands_rx.take().unwrap();
|
let mut commands_rx = self.commands_rx.take().unwrap();
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
_ = self.run_supervised_loop() => {
|
_ = self.run_supervised_loop() => {
|
||||||
// the supervised loop cannot fail. If it finished, don't restart.
|
// the supervised loop cannot fail. If it finished, don't restart.
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
cmd = commands_rx.recv() => {
|
cmd = commands_rx.recv() => {
|
||||||
match cmd {
|
match cmd {
|
||||||
Ok(ListenerCommand::Restart) => {
|
Some(ListenerCommand::Restart) => {
|
||||||
info!("Received restart command");
|
info!("Received restart command");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ok(ListenerCommand::Shutdown) => {
|
Some(ListenerCommand::Shutdown) => {
|
||||||
info!("Received shutdown command");
|
info!("Received shutdown command");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Some(ListenerCommand::GetState(tx)) => {
|
||||||
error!("Command receive error: {:?}", e);
|
info!("Received get state command");
|
||||||
break;
|
let state = self.state.clone();
|
||||||
}
|
let _ = tx.send(state);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!("Channel closed for ListenerActor");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_state(&mut self, state: ConnectionState) {
|
async fn set_state(&mut self, state: ConnectionState) {
|
||||||
self.state.replace(state.clone());
|
self.state = state.clone();
|
||||||
self.event_tx
|
self.event_tx.send(ListenerEvent::ConnectionStateChanged(state)).await.unwrap();
|
||||||
.send(ListenerEvent::ConnectionStateChanged(state)).unwrap();
|
|
||||||
}
|
}
|
||||||
async fn run_supervised_loop(&mut self) {
|
async fn run_supervised_loop(&mut self) {
|
||||||
dbg!("supervised");
|
dbg!("supervised");
|
||||||
@ -189,7 +208,7 @@ impl ConnectionHandler {
|
|||||||
retry_count: retry.count(),
|
retry_count: retry.count(),
|
||||||
delay: retry.next_delay(),
|
delay: retry.next_delay(),
|
||||||
error: Some(Arc::new(e)),
|
error: Some(Arc::new(e)),
|
||||||
});
|
}).await;
|
||||||
info!(delay = ?retry.next_delay(), "restarting");
|
info!(delay = ?retry.next_delay(), "restarting");
|
||||||
retry.wait().await;
|
retry.wait().await;
|
||||||
} else {
|
} else {
|
||||||
@ -219,12 +238,11 @@ impl ConnectionHandler {
|
|||||||
|
|
||||||
self.set_state(
|
self.set_state(
|
||||||
ConnectionState::Connected,
|
ConnectionState::Connected,
|
||||||
);
|
).await;
|
||||||
|
|
||||||
info!(topic = %&self.config.topic, "listening");
|
info!(topic = %&self.config.topic, "listening");
|
||||||
while let Some(msg) = stream.next().await {
|
while let Some(msg) = stream.next().await {
|
||||||
let msg = msg?;
|
let msg = msg?;
|
||||||
dbg!(&msg);
|
|
||||||
|
|
||||||
let min_msg = serde_json::from_str::<models::MinMessage>(&msg)
|
let min_msg = serde_json::from_str::<models::MinMessage>(&msg)
|
||||||
.map_err(|e| Error::InvalidMinMessage(msg.to_string(), e))?;
|
.map_err(|e| Error::InvalidMinMessage(msg.to_string(), e))?;
|
||||||
@ -234,9 +252,9 @@ impl ConnectionHandler {
|
|||||||
.map_err(|e| Error::InvalidMessage(msg.to_string(), e))?;
|
.map_err(|e| Error::InvalidMessage(msg.to_string(), e))?;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
ServerEvent::Message { message, .. } => {
|
ServerEvent::Message(msg) => {
|
||||||
debug!("message event");
|
debug!("message event");
|
||||||
self.event_tx.send(ListenerEvent::Message(message))?;
|
self.event_tx.send(ListenerEvent::Message(msg)).await.unwrap();
|
||||||
}
|
}
|
||||||
ServerEvent::KeepAlive { .. } => {
|
ServerEvent::KeepAlive { .. } => {
|
||||||
debug!("keepalive event");
|
debug!("keepalive event");
|
||||||
@ -253,61 +271,27 @@ impl ConnectionHandler {
|
|||||||
|
|
||||||
// Reliable listener implementation
|
// Reliable listener implementation
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Listener {
|
pub struct ListenerHandle {
|
||||||
pub state: Rc<RefCell<ConnectionState>>,
|
pub events: async_channel::Receiver<ListenerEvent>,
|
||||||
pub events: watch::Receiver<ListenerEvent>,
|
|
||||||
pub config: ListenerConfig,
|
pub config: ListenerConfig,
|
||||||
pub commands: broadcast::Sender<ListenerCommand>,
|
pub commands: mpsc::Sender<ListenerCommand>,
|
||||||
pub event_tracker: OutputTracker<ListenerEvent>,
|
join_handle: Arc<Option<task::JoinHandle<()>>>,
|
||||||
local_set: Rc<LocalSet>,
|
listener_actor: Arc<RwLock<Option<ListenerActor>>>,
|
||||||
connection_handler: Rc<RefCell<Option<ConnectionHandler>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Listener {
|
impl ListenerHandle {
|
||||||
pub fn new(config: ListenerConfig) -> Self {
|
// the response will be sent as an event in self.events
|
||||||
let (tx, rx) = watch::channel(ListenerEvent::ConnectionStateChanged(
|
pub async fn request_state(&self) -> ConnectionState {
|
||||||
ConnectionState::Unitialized,
|
let (tx, rx) = oneshot::channel();
|
||||||
));
|
self.commands.send(ListenerCommand::GetState(tx)).await.unwrap();
|
||||||
let (commands_tx, commands_rx) = broadcast::channel(1);
|
rx.await.unwrap()
|
||||||
|
|
||||||
let local_set = Rc::new(LocalSet::new());
|
|
||||||
let connection_handler = ConnectionHandler::new(config.clone(), tx, commands_rx);
|
|
||||||
let state = connection_handler.state.clone();
|
|
||||||
|
|
||||||
let event_tracker = OutputTracker::default();
|
|
||||||
// let event_tracker_clone = event_tracker.clone();
|
|
||||||
// let mut rx_clone = rx.clone();
|
|
||||||
// local_set.spawn_local(async move {
|
|
||||||
// rx_clone.changed().await.unwrap();
|
|
||||||
// event_tracker_clone.push(rx_clone.borrow().clone());
|
|
||||||
// });
|
|
||||||
|
|
||||||
Listener {
|
|
||||||
state,
|
|
||||||
events: rx,
|
|
||||||
config,
|
|
||||||
commands: commands_tx,
|
|
||||||
local_set,
|
|
||||||
event_tracker,
|
|
||||||
connection_handler: Rc::new(RefCell::new(Some(connection_handler))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub async fn run(&mut self) {
|
|
||||||
let connection_handler = self.connection_handler.take().unwrap();
|
|
||||||
|
|
||||||
let _ = self
|
|
||||||
.local_set
|
|
||||||
.run_until(async move {
|
|
||||||
connection_handler.run().await.unwrap();
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use models::Subscription;
|
use models::Subscription;
|
||||||
use reqwest::ResponseBuilderExt;
|
use serde_json::json;
|
||||||
use task::LocalSet;
|
use task::LocalSet;
|
||||||
use tokio_stream::wrappers::WatchStream;
|
use tokio_stream::wrappers::WatchStream;
|
||||||
|
|
||||||
@ -328,34 +312,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_listener_reconnects_on_http_status_400() {
|
async fn test_listener_reconnects_on_http_status_500() {
|
||||||
let local_set = LocalSet::new();
|
let local_set = LocalSet::new();
|
||||||
local_set
|
local_set
|
||||||
.run_until(async {
|
.spawn_local(async {
|
||||||
let http_client = HttpClient::new_nullable({
|
let http_client = HttpClient::new_nullable({
|
||||||
let nullable = NullableClient::new();
|
|
||||||
let url = Subscription::build_url("http://localhost", "test", 0).unwrap();
|
let url = Subscription::build_url("http://localhost", "test", 0).unwrap();
|
||||||
nullable
|
let nullable = NullableClient::builder()
|
||||||
.set_response(
|
.text_response(url.clone(), 500, "failed")
|
||||||
url.as_str(),
|
.json_response(url, 200, json!({"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"})).unwrap()
|
||||||
reqwest::Response::from(
|
.build();
|
||||||
http::response::Builder::new()
|
|
||||||
.status(500)
|
|
||||||
.url(url.clone())
|
|
||||||
.body("failed")
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
nullable.set_default_response(Box::new(move || {
|
|
||||||
reqwest::Response::from(
|
|
||||||
http::response::Builder::new()
|
|
||||||
.status(200)
|
|
||||||
.url(url.clone())
|
|
||||||
.body(r#"{"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}"#)
|
|
||||||
.unwrap(),
|
|
||||||
)})
|
|
||||||
).await;
|
|
||||||
nullable
|
nullable
|
||||||
});
|
});
|
||||||
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
||||||
@ -368,11 +334,8 @@ mod tests {
|
|||||||
since: 0,
|
since: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut listener = Listener::new(config.clone());
|
let mut listener = ListenerActor::new(config.clone());
|
||||||
let events = listener.events.clone();
|
let items: Vec<_> = listener.events.take(3).collect().await;
|
||||||
let changes = WatchStream::new(events);
|
|
||||||
spawn_local(async move { listener.run().await });
|
|
||||||
let items: Vec<_> = changes.take(3).collect().await;
|
|
||||||
|
|
||||||
|
|
||||||
dbg!(&items);
|
dbg!(&items);
|
||||||
@ -391,40 +354,21 @@ mod tests {
|
|||||||
// ListenerEvent::Disconnected { .. },
|
// ListenerEvent::Disconnected { .. },
|
||||||
// ListenerEvent::Connected { .. },
|
// ListenerEvent::Connected { .. },
|
||||||
// ));
|
// ));
|
||||||
})
|
});
|
||||||
.await;
|
local_set.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_listener_reconnects_on_invalid_message() {
|
async fn test_listener_reconnects_on_invalid_message() {
|
||||||
let local_set = LocalSet::new();
|
let local_set = LocalSet::new();
|
||||||
local_set
|
local_set
|
||||||
.run_until(async {
|
.spawn_local(async {
|
||||||
let http_client = HttpClient::new_nullable({
|
let http_client = HttpClient::new_nullable({
|
||||||
let nullable = NullableClient::new();
|
|
||||||
let url = Subscription::build_url("http://localhost", "test", 0).unwrap();
|
let url = Subscription::build_url("http://localhost", "test", 0).unwrap();
|
||||||
nullable
|
let nullable = NullableClient::builder()
|
||||||
.set_response(
|
.text_response(url.clone(), 200, "invalid message")
|
||||||
url.as_str(),
|
.json_response(url, 200, json!({"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"})).unwrap()
|
||||||
reqwest::Response::from(
|
.build();
|
||||||
http::response::Builder::new()
|
|
||||||
.status(200)
|
|
||||||
.url(url.clone())
|
|
||||||
.body("failed")
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
nullable.set_default_response(Box::new(move || {
|
|
||||||
reqwest::Response::from(
|
|
||||||
http::response::Builder::new()
|
|
||||||
.status(200)
|
|
||||||
.url(url.clone())
|
|
||||||
.body(r#"{"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}"#)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
})).await;
|
|
||||||
|
|
||||||
nullable
|
nullable
|
||||||
});
|
});
|
||||||
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
||||||
@ -437,11 +381,8 @@ mod tests {
|
|||||||
since: 0,
|
since: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut listener = Listener::new(config.clone());
|
let mut listener = ListenerActor::new(config.clone());
|
||||||
let events = listener.events.clone();
|
let items: Vec<_> = listener.events.take(3).collect().await;
|
||||||
let changes = WatchStream::new(events);
|
|
||||||
spawn_local(async move { listener.run().await });
|
|
||||||
let items: Vec<_> = changes.take(3).collect().await;
|
|
||||||
|
|
||||||
dbg!(&items);
|
dbg!(&items);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@ -452,15 +393,15 @@ mod tests {
|
|||||||
ListenerEvent::ConnectionStateChanged(ConnectionState::Connected { .. }),
|
ListenerEvent::ConnectionStateChanged(ConnectionState::Connected { .. }),
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
})
|
});
|
||||||
.await;
|
local_set.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn integration_connects_sends_receives_simple() {
|
async fn integration_connects_sends_receives_simple() {
|
||||||
let local_set = LocalSet::new();
|
let local_set = LocalSet::new();
|
||||||
local_set
|
local_set
|
||||||
.run_until(async {
|
.spawn_local(async {
|
||||||
let http_client = HttpClient::new(reqwest::Client::new());
|
let http_client = HttpClient::new(reqwest::Client::new());
|
||||||
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
let credentials = Credentials::new_nullable(vec![]).await.unwrap();
|
||||||
|
|
||||||
@ -472,10 +413,10 @@ mod tests {
|
|||||||
since: 0,
|
since: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut listener = Listener::new(config.clone());
|
let mut listener = ListenerActor::new(config.clone());
|
||||||
|
|
||||||
// assert_event_matches!(listener, ListenerEvent::Connected { .. },);
|
// assert_event_matches!(listener, ListenerEvent::Connected { .. },);
|
||||||
})
|
});
|
||||||
.await;
|
local_set.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use std::sync::{Arc, RwLock};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use rusqlite::{params, Connection, Result};
|
use rusqlite::{params, Connection, Result};
|
||||||
@ -8,16 +9,16 @@ use crate::Error;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
conn: Rc<RefCell<Connection>>,
|
conn: Arc<RwLock<Connection>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub fn connect(path: &str) -> Result<Self> {
|
pub fn connect(path: &str) -> Result<Self> {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
conn: Rc::new(RefCell::new(Connection::open(path)?)),
|
conn: Arc::new(RwLock::new(Connection::open(path)?)),
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
this.conn.borrow().execute_batch(
|
this.conn.read().unwrap().execute_batch(
|
||||||
"PRAGMA foreign_keys = ON;
|
"PRAGMA foreign_keys = ON;
|
||||||
PRAGMA journal_mode = wal;",
|
PRAGMA journal_mode = wal;",
|
||||||
)?;
|
)?;
|
||||||
@ -27,12 +28,12 @@ impl Db {
|
|||||||
}
|
}
|
||||||
fn migrate(&mut self) -> Result<()> {
|
fn migrate(&mut self) -> Result<()> {
|
||||||
self.conn
|
self.conn
|
||||||
.borrow()
|
.read().unwrap()
|
||||||
.execute_batch(include_str!("./migrations/00.sql"))?;
|
.execute_batch(include_str!("./migrations/00.sql"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn get_or_insert_server(&mut self, server: &str) -> Result<i64> {
|
fn get_or_insert_server(&mut self, server: &str) -> Result<i64> {
|
||||||
let mut conn = self.conn.borrow_mut();
|
let mut conn = self.conn.write().unwrap();
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
let mut res = tx.query_row(
|
let mut res = tx.query_row(
|
||||||
"SELECT id
|
"SELECT id
|
||||||
@ -56,7 +57,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
pub fn insert_message(&mut self, server: &str, json_data: &str) -> Result<(), Error> {
|
pub fn insert_message(&mut self, server: &str, json_data: &str) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(server)?;
|
let server_id = self.get_or_insert_server(server)?;
|
||||||
let res = self.conn.borrow().execute(
|
let res = self.conn.read().unwrap().execute(
|
||||||
"INSERT INTO message (server, data) VALUES (?1, ?2)",
|
"INSERT INTO message (server, data) VALUES (?1, ?2)",
|
||||||
params![server_id, json_data],
|
params![server_id, json_data],
|
||||||
);
|
);
|
||||||
@ -76,7 +77,7 @@ impl Db {
|
|||||||
topic: &str,
|
topic: &str,
|
||||||
since: u64,
|
since: u64,
|
||||||
) -> Result<Vec<String>, rusqlite::Error> {
|
) -> Result<Vec<String>, rusqlite::Error> {
|
||||||
let conn = self.conn.borrow();
|
let conn = self.conn.read().unwrap();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"
|
"
|
||||||
SELECT data
|
SELECT data
|
||||||
@ -94,7 +95,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
pub fn insert_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
pub fn insert_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(&sub.server)?;
|
let server_id = self.get_or_insert_server(&sub.server)?;
|
||||||
self.conn.borrow().execute(
|
self.conn.read().unwrap().execute(
|
||||||
"INSERT INTO subscription (server, topic, display_name, reserved, muted, archived) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
"INSERT INTO subscription (server, topic, display_name, reserved, muted, archived) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
params![
|
params![
|
||||||
server_id,
|
server_id,
|
||||||
@ -109,7 +110,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
pub fn remove_subscription(&mut self, server: &str, topic: &str) -> Result<(), Error> {
|
pub fn remove_subscription(&mut self, server: &str, topic: &str) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(server)?;
|
let server_id = self.get_or_insert_server(server)?;
|
||||||
let res = self.conn.borrow().execute(
|
let res = self.conn.read().unwrap().execute(
|
||||||
"DELETE FROM subscription
|
"DELETE FROM subscription
|
||||||
WHERE server = ?1 AND topic = ?2",
|
WHERE server = ?1 AND topic = ?2",
|
||||||
params![server_id, topic],
|
params![server_id, topic],
|
||||||
@ -120,7 +121,7 @@ impl Db {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn list_subscriptions(&mut self) -> Result<Vec<models::Subscription>, Error> {
|
pub fn list_subscriptions(&mut self) -> Result<Vec<models::Subscription>, Error> {
|
||||||
let conn = self.conn.borrow();
|
let conn = self.conn.read().unwrap();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT server.endpoint, sub.topic, sub.display_name, sub.reserved, sub.muted, sub.archived, sub.symbolic_icon, sub.read_until
|
"SELECT server.endpoint, sub.topic, sub.display_name, sub.reserved, sub.muted, sub.archived, sub.symbolic_icon, sub.read_until
|
||||||
FROM subscription sub
|
FROM subscription sub
|
||||||
@ -146,7 +147,7 @@ impl Db {
|
|||||||
|
|
||||||
pub fn update_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
pub fn update_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(&sub.server)?;
|
let server_id = self.get_or_insert_server(&sub.server)?;
|
||||||
let res = self.conn.borrow().execute(
|
let res = self.conn.read().unwrap().execute(
|
||||||
"UPDATE subscription
|
"UPDATE subscription
|
||||||
SET display_name = ?1, reserved = ?2, muted = ?3, archived = ?4, read_until = ?5
|
SET display_name = ?1, reserved = ?2, muted = ?3, archived = ?4, read_until = ?5
|
||||||
WHERE server = ?6 AND topic = ?7",
|
WHERE server = ?6 AND topic = ?7",
|
||||||
@ -174,7 +175,7 @@ impl Db {
|
|||||||
value: u64,
|
value: u64,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(server).unwrap();
|
let server_id = self.get_or_insert_server(server).unwrap();
|
||||||
let conn = self.conn.borrow();
|
let conn = self.conn.read().unwrap();
|
||||||
let res = conn.execute(
|
let res = conn.execute(
|
||||||
"UPDATE subscription
|
"UPDATE subscription
|
||||||
SET read_until = ?3
|
SET read_until = ?3
|
||||||
@ -189,7 +190,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
pub fn delete_messages(&mut self, server: &str, topic: &str) -> Result<(), Error> {
|
pub fn delete_messages(&mut self, server: &str, topic: &str) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(server).unwrap();
|
let server_id = self.get_or_insert_server(server).unwrap();
|
||||||
let conn = self.conn.borrow();
|
let conn = self.conn.read().unwrap();
|
||||||
let res = conn.execute(
|
let res = conn.execute(
|
||||||
"DELETE FROM message
|
"DELETE FROM message
|
||||||
WHERE topic = ?2 AND server = ?1
|
WHERE topic = ?2 AND server = ?1
|
||||||
|
|||||||
@ -28,7 +28,9 @@ pub fn validate_topic(topic: &str) -> Result<&str, Error> {
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
|
pub id: String,
|
||||||
pub topic: String,
|
pub topic: String,
|
||||||
|
pub expires: Option<u64>,
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
#[serde(default = "Default::default")]
|
#[serde(default = "Default::default")]
|
||||||
pub time: u64,
|
pub time: u64,
|
||||||
@ -337,3 +339,35 @@ pub trait NotificationProxy: Sync + Send {
|
|||||||
pub trait NetworkMonitorProxy: Sync + Send {
|
pub trait NetworkMonitorProxy: Sync + Send {
|
||||||
fn listen(&self) -> Pin<Box<dyn Stream<Item = ()>>>;
|
fn listen(&self) -> Pin<Box<dyn Stream<Item = ()>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct NullNotifier {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NullNotifier {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl NotificationProxy for NullNotifier {
|
||||||
|
fn send(&self, n: Notification) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NullNetworkMonitor {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NullNetworkMonitor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkMonitorProxy for NullNetworkMonitor {
|
||||||
|
fn listen(&self) -> Pin<Box<dyn Stream<Item = ()>>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,48 +1,89 @@
|
|||||||
|
use crate::models::NullNetworkMonitor;
|
||||||
|
use crate::models::NullNotifier;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use std::{collections::HashMap, future::Future, sync::Arc};
|
use std::{collections::HashMap, future::Future, sync::Arc};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{broadcast, mpsc, RwLock},
|
sync::{broadcast, mpsc, oneshot, RwLock},
|
||||||
task::LocalSet,
|
task::{spawn_local, LocalSet},
|
||||||
};
|
};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credentials::{self, Credential},
|
|
||||||
http_client::HttpClient,
|
http_client::HttpClient,
|
||||||
listener::{Listener, ListenerCommand, ListenerConfig, ListenerEvent},
|
|
||||||
message_repo::Db,
|
message_repo::Db,
|
||||||
models::{self, Account},
|
models::{self, Account},
|
||||||
topic_listener::build_client,
|
topic_listener::build_client,
|
||||||
SharedEnv,
|
ListenerActor, ListenerCommand, ListenerConfig, ListenerHandle, SharedEnv, SubscriptionHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Message types for the actor
|
||||||
|
#[derive()]
|
||||||
|
pub enum NtfyMessage {
|
||||||
|
Subscribe {
|
||||||
|
server: String,
|
||||||
|
topic: String,
|
||||||
|
respond_to: oneshot::Sender<Result<SubscriptionHandle, Vec<anyhow::Error>>>,
|
||||||
|
},
|
||||||
|
Unsubscribe {
|
||||||
|
server: String,
|
||||||
|
topic: String,
|
||||||
|
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
RefreshAll {
|
||||||
|
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
ListSubscriptions {
|
||||||
|
respond_to: oneshot::Sender<anyhow::Result<Vec<SubscriptionHandle>>>,
|
||||||
|
},
|
||||||
|
ListAccounts {
|
||||||
|
respond_to: oneshot::Sender<anyhow::Result<Vec<Account>>>,
|
||||||
|
},
|
||||||
|
WatchSubscribed {
|
||||||
|
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct WatchKey {
|
pub struct WatchKey {
|
||||||
server: String,
|
server: String,
|
||||||
topic: String,
|
topic: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct NtfyActor {
|
||||||
pub struct Ntfy {
|
listener_handles: Arc<RwLock<HashMap<WatchKey, SubscriptionHandle>>>,
|
||||||
listener_handles: Arc<RwLock<HashMap<WatchKey, Listener>>>,
|
|
||||||
env: SharedEnv,
|
env: SharedEnv,
|
||||||
|
command_rx: mpsc::Receiver<NtfyMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ntfy {
|
#[derive(Clone)]
|
||||||
pub fn new(env: SharedEnv) -> Self {
|
pub struct NtfyHandle {
|
||||||
Self {
|
command_tx: mpsc::Sender<NtfyMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NtfyActor {
|
||||||
|
pub fn new(env: SharedEnv) -> (Self, NtfyHandle) {
|
||||||
|
let (command_tx, command_rx) = mpsc::channel(32);
|
||||||
|
|
||||||
|
let actor = Self {
|
||||||
listener_handles: Default::default(),
|
listener_handles: Default::default(),
|
||||||
env,
|
env,
|
||||||
}
|
command_rx,
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = NtfyHandle { command_tx };
|
||||||
|
|
||||||
|
(actor, handle)
|
||||||
}
|
}
|
||||||
pub async fn subscribe(
|
|
||||||
|
async fn handle_subscribe(
|
||||||
&self,
|
&self,
|
||||||
server: &str,
|
server: String,
|
||||||
topic: &str,
|
topic: String,
|
||||||
) -> Result<Listener, Vec<anyhow::Error>> {
|
) -> Result<SubscriptionHandle, Vec<anyhow::Error>> {
|
||||||
let subscription = models::Subscription::builder(topic.to_owned())
|
let subscription = models::Subscription::builder(topic.clone())
|
||||||
.server(server.to_string())
|
.server(server.clone())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.into_iter().map(|e| anyhow!(e)).collect::<Vec<_>>())?;
|
.map_err(|e| e.into_iter().map(|e| anyhow!(e)).collect::<Vec<_>>())?;
|
||||||
|
|
||||||
@ -50,81 +91,94 @@ impl Ntfy {
|
|||||||
db.insert_subscription(subscription.clone())
|
db.insert_subscription(subscription.clone())
|
||||||
.map_err(|e| vec![anyhow!(e)])?;
|
.map_err(|e| vec![anyhow!(e)])?;
|
||||||
|
|
||||||
let listener = self.listen(subscription).await;
|
self.listen(subscription)
|
||||||
listener.map_err(|e| vec![anyhow!(e)])
|
.await
|
||||||
|
.map_err(|e| vec![anyhow!(e)])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsubscribe(&mut self, server: &str, topic: &str) -> anyhow::Result<()> {
|
async fn handle_unsubscribe(&mut self, server: String, topic: String) -> anyhow::Result<()> {
|
||||||
let listener = self.listener_handles.write().await.remove(&WatchKey {
|
let subscription = self.listener_handles.write().await.remove(&WatchKey {
|
||||||
server: server.to_string(),
|
server: server.clone(),
|
||||||
topic: topic.to_string(),
|
topic: topic.clone(),
|
||||||
});
|
});
|
||||||
if let Some(listener) = listener {
|
|
||||||
listener.commands.send(ListenerCommand::Shutdown)?;
|
if let Some(sub) = subscription {
|
||||||
|
sub.shutdown().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.env.db.remove_subscription(server, topic)?;
|
self.env.db.remove_subscription(&server, &topic)?;
|
||||||
info!(server, topic, "Unsubscribed");
|
info!(server, topic, "Unsubscribed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO rename reconnect_all
|
pub async fn run(&mut self) {
|
||||||
pub async fn refresh_all(&mut self) -> anyhow::Result<()> {
|
while let Some(msg) = self.command_rx.recv().await {
|
||||||
for listener in self.listener_handles.read().await.values() {
|
match msg {
|
||||||
listener.commands.send(ListenerCommand::Restart)?;
|
NtfyMessage::Subscribe {
|
||||||
}
|
server,
|
||||||
Ok(())
|
topic,
|
||||||
}
|
respond_to,
|
||||||
|
} => {
|
||||||
|
let result = self.handle_subscribe(server, topic).await;
|
||||||
|
let _ = respond_to.send(result);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_subscriptions(&mut self) -> anyhow::Result<Vec<Listener>> {
|
NtfyMessage::Unsubscribe {
|
||||||
let values = self
|
server,
|
||||||
.listener_handles
|
topic,
|
||||||
.read()
|
respond_to,
|
||||||
.await
|
} => {
|
||||||
.values()
|
let result = self.handle_unsubscribe(server, topic).await;
|
||||||
.cloned()
|
let _ = respond_to.send(result);
|
||||||
.collect::<Vec<_>>();
|
}
|
||||||
|
|
||||||
Ok(values)
|
NtfyMessage::RefreshAll { respond_to } => {
|
||||||
}
|
let mut res = Ok(());
|
||||||
|
for sub in self.listener_handles.read().await.values() {
|
||||||
|
res = sub.restart().await;
|
||||||
|
if res.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = respond_to.send(res);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_accounts(&mut self) -> anyhow::Result<Vec<Account>> {
|
NtfyMessage::ListSubscriptions { respond_to } => {
|
||||||
let values = self.env.credentials.list_all();
|
let subs = self
|
||||||
let res = values
|
.listener_handles
|
||||||
.into_iter()
|
.read()
|
||||||
.map(|(server, credential)| Account {
|
.await
|
||||||
server,
|
.values()
|
||||||
username: credential.username,
|
.cloned()
|
||||||
})
|
.collect();
|
||||||
.collect();
|
let _ = respond_to.send(Ok(subs));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(res)
|
NtfyMessage::ListAccounts { respond_to } => {
|
||||||
}
|
let accounts = self
|
||||||
|
.env
|
||||||
|
.credentials
|
||||||
|
.list_all()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(server, credential)| Account {
|
||||||
|
server,
|
||||||
|
username: credential.username,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let _ = respond_to.send(Ok(accounts));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn listen(
|
NtfyMessage::WatchSubscribed { respond_to } => {
|
||||||
&self,
|
let result = self.handle_watch_subscribed().await;
|
||||||
sub: models::Subscription,
|
let _ = respond_to.send(result);
|
||||||
) -> impl Future<Output = anyhow::Result<Listener>> {
|
}
|
||||||
let server = sub.server.clone();
|
|
||||||
let topic = sub.topic.clone();
|
NtfyMessage::Shutdown => break,
|
||||||
let listener = Listener::new(ListenerConfig {
|
}
|
||||||
http_client: self.env.nullable_http.clone(),
|
|
||||||
credentials: self.env.credentials.clone(),
|
|
||||||
endpoint: server.clone(),
|
|
||||||
topic: topic.clone(),
|
|
||||||
since: sub.read_until,
|
|
||||||
});
|
|
||||||
let listener_handles = self.listener_handles.clone();
|
|
||||||
async move {
|
|
||||||
listener_handles
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(WatchKey { server, topic }, listener.clone());
|
|
||||||
Ok(listener)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn watch_subscribed(&mut self) -> anyhow::Result<()> {
|
async fn handle_watch_subscribed(&mut self) -> anyhow::Result<()> {
|
||||||
let f: Vec<_> = self
|
let f: Vec<_> = self
|
||||||
.env
|
.env
|
||||||
.db
|
.db
|
||||||
@ -132,48 +186,227 @@ impl Ntfy {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|m| self.listen(m))
|
.map(|m| self.listen(m))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
join_all(f.into_iter().map(|x| async move {
|
join_all(f.into_iter().map(|x| async move {
|
||||||
if let Err(e) = x.await {
|
if let Err(e) = x.await {
|
||||||
error!(error = ?e, "Can't rewatch subscribed topic");
|
error!(error = ?e, "Can't rewatch subscribed topic");
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_account(&mut self) {}
|
fn listen(
|
||||||
fn remove_account(&mut self) {}
|
&self,
|
||||||
|
sub: models::Subscription,
|
||||||
|
) -> impl Future<Output = anyhow::Result<SubscriptionHandle>> {
|
||||||
|
let server = sub.server.clone();
|
||||||
|
let topic = sub.topic.clone();
|
||||||
|
let listener = ListenerActor::new(ListenerConfig {
|
||||||
|
http_client: self.env.nullable_http.clone(),
|
||||||
|
credentials: self.env.credentials.clone(),
|
||||||
|
endpoint: server.clone(),
|
||||||
|
topic: topic.clone(),
|
||||||
|
since: sub.read_until,
|
||||||
|
});
|
||||||
|
let listener_handles = self.listener_handles.clone();
|
||||||
|
let sub = SubscriptionHandle::new(listener.clone(), sub, &self.env);
|
||||||
|
|
||||||
|
async move {
|
||||||
|
listener_handles
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(WatchKey { server, topic }, sub.clone());
|
||||||
|
Ok(sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NtfyHandle {
|
||||||
|
pub async fn subscribe(
|
||||||
|
&self,
|
||||||
|
server: &str,
|
||||||
|
topic: &str,
|
||||||
|
) -> Result<SubscriptionHandle, Vec<anyhow::Error>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::Subscribe {
|
||||||
|
server: server.to_string(),
|
||||||
|
topic: topic.to_string(),
|
||||||
|
respond_to: tx,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| vec![anyhow!("Actor mailbox error")])?;
|
||||||
|
|
||||||
|
rx.await
|
||||||
|
.map_err(|_| vec![anyhow!("Actor response error")])?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unsubscribe(&self, server: &str, topic: &str) -> anyhow::Result<()> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::Unsubscribe {
|
||||||
|
server: server.to_string(),
|
||||||
|
topic: topic.to_string(),
|
||||||
|
respond_to: tx,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
||||||
|
|
||||||
|
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_all(&self) -> anyhow::Result<()> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::RefreshAll { respond_to: tx })
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
||||||
|
|
||||||
|
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_subscriptions(&self) -> anyhow::Result<Vec<SubscriptionHandle>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::ListSubscriptions { respond_to: tx })
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
||||||
|
|
||||||
|
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_accounts(&self) -> anyhow::Result<Vec<Account>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::ListAccounts { respond_to: tx })
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
||||||
|
|
||||||
|
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn watch_subscribed(&self) -> anyhow::Result<()> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.command_tx
|
||||||
|
.send(NtfyMessage::WatchSubscribed { respond_to: tx })
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
||||||
|
|
||||||
|
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
socket_path: std::path::PathBuf,
|
socket_path: std::path::PathBuf,
|
||||||
dbpath: &str,
|
dbpath: &str,
|
||||||
notification_proxy: Arc<dyn models::NotificationProxy>,
|
notification_proxy: Arc<dyn models::NotificationProxy>,
|
||||||
network_proxy: Arc<dyn models::NetworkMonitorProxy>,
|
network_proxy: Arc<dyn models::NetworkMonitorProxy>,
|
||||||
) -> anyhow::Result<Ntfy> {
|
) -> anyhow::Result<NtfyHandle> {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let dbpath = dbpath.to_owned();
|
let dbpath = dbpath.to_owned();
|
||||||
let credentials = rt.block_on(async { crate::credentials::Credentials::new().await.unwrap() });
|
|
||||||
let local = tokio::task::LocalSet::new();
|
|
||||||
|
|
||||||
let env = SharedEnv {
|
// Create a channel to receive the handle from the spawned thread
|
||||||
db: Db::connect(&dbpath).unwrap(),
|
let (handle_tx, handle_rx) = oneshot::channel();
|
||||||
proxy: notification_proxy,
|
|
||||||
http: build_client().unwrap(),
|
std::thread::spawn(move || {
|
||||||
nullable_http: HttpClient::new(build_client().unwrap()),
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
network: network_proxy,
|
.enable_all()
|
||||||
credentials,
|
.build()
|
||||||
};
|
.unwrap();
|
||||||
let ntfy = Ntfy::new(env);
|
|
||||||
let mut ntfy_clone = ntfy.clone();
|
// Create everything inside the new thread's runtime
|
||||||
local.spawn_local(async move {
|
let credentials =
|
||||||
ntfy_clone.watch_subscribed().await.unwrap();
|
rt.block_on(async move { crate::credentials::Credentials::new().await.unwrap() });
|
||||||
|
|
||||||
|
let env = SharedEnv {
|
||||||
|
db: Db::connect(&dbpath).unwrap(),
|
||||||
|
proxy: notification_proxy,
|
||||||
|
http: build_client().unwrap(),
|
||||||
|
nullable_http: HttpClient::new(build_client().unwrap()),
|
||||||
|
network: network_proxy,
|
||||||
|
credentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut actor, handle) = NtfyActor::new(env);
|
||||||
|
let handle_clone = handle.clone();
|
||||||
|
|
||||||
|
// Send the handle back to the calling thread
|
||||||
|
handle_tx.send(handle.clone());
|
||||||
|
|
||||||
|
rt.block_on({
|
||||||
|
let local_set = LocalSet::new();
|
||||||
|
// Spawn the watch_subscribed task
|
||||||
|
local_set.spawn_local(async move {
|
||||||
|
if let Err(e) = handle_clone.watch_subscribed().await {
|
||||||
|
error!(error = ?e, "Failed to watch subscribed topics");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the actor
|
||||||
|
local_set.spawn_local(async move {
|
||||||
|
actor.run().await;
|
||||||
|
});
|
||||||
|
local_set
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(ntfy)
|
// Wait for the handle from the spawned thread
|
||||||
|
Ok(handle_rx
|
||||||
|
.blocking_recv()
|
||||||
|
.map_err(|_| anyhow!("Failed to receive actor handle"))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use models::Message;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::ListenerEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_subscribe_and_publish() {
|
||||||
|
let notification_proxy = Arc::new(NullNotifier::new());
|
||||||
|
let network_proxy = Arc::new(NullNetworkMonitor::new());
|
||||||
|
let dbpath = ":memory:";
|
||||||
|
let socket_path = std::path::PathBuf::from("/tmp/ntfy.sock");
|
||||||
|
|
||||||
|
let handle = start(socket_path, dbpath, notification_proxy, network_proxy).unwrap();
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
let server = "http://localhost:8000";
|
||||||
|
let topic = "test_topic";
|
||||||
|
|
||||||
|
// Subscribe to the topic
|
||||||
|
let subscription_handle = handle.subscribe(server, topic).await.unwrap();
|
||||||
|
|
||||||
|
// Publish a message
|
||||||
|
let message = serde_json::to_string(&Message {
|
||||||
|
topic: topic.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let result = subscription_handle.publish(message).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
|
||||||
|
// Attach to the subscription and check if the message is received and stored
|
||||||
|
let (events, receiver) = subscription_handle.attach().await;
|
||||||
|
dbg!(&events);
|
||||||
|
assert!(events.iter().any(|event| match event {
|
||||||
|
ListenerEvent::Message(msg) => msg.topic == topic,
|
||||||
|
_ => false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
257
ntfy-daemon/src/subscription.rs
Normal file
257
ntfy-daemon/src/subscription.rs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
use crate::listener::{ListenerEvent, ListenerHandle};
|
||||||
|
use crate::message_repo::Db;
|
||||||
|
use crate::models::{self, Message, NotificationProxy};
|
||||||
|
use crate::{Error, ServerEvent, SharedEnv};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::select;
|
||||||
|
use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock};
|
||||||
|
use tokio::task::spawn_local;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SubscriptionHandle {
|
||||||
|
sender: mpsc::Sender<SubscriptionRequest>,
|
||||||
|
listener: ListenerHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubscriptionHandle {
|
||||||
|
pub fn new(listener: ListenerHandle, model: models::Subscription, env: &SharedEnv) -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel(32);
|
||||||
|
let broadcast_tx = broadcast::channel(8).0;
|
||||||
|
let actor = SubscriptionActor {
|
||||||
|
listener: listener.clone(),
|
||||||
|
model,
|
||||||
|
receiver,
|
||||||
|
env: env.clone(),
|
||||||
|
broadcast_tx: broadcast_tx.clone(),
|
||||||
|
};
|
||||||
|
spawn_local(actor.run());
|
||||||
|
Self { sender, listener }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn model(&self) -> models::Subscription {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::GetModel { resp_tx })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_info(&self, new_model: models::Subscription) -> anyhow::Result<()> {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::UpdateInfo { new_model, resp_tx })
|
||||||
|
.await?;
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart(&self) -> anyhow::Result<()> {
|
||||||
|
self.listener
|
||||||
|
.commands
|
||||||
|
.send(crate::ListenerCommand::Restart)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(&self) -> anyhow::Result<()> {
|
||||||
|
self.listener
|
||||||
|
.commands
|
||||||
|
.send(crate::ListenerCommand::Shutdown)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a vector containing all the past messages stored in the database and the current connection state.
|
||||||
|
// The first vector is useful to get a summary of what happened before.
|
||||||
|
// The `ListenerHandle` is returned to receive new events.
|
||||||
|
pub async fn attach(&self) -> (Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>) {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::Attach { resp_tx })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish(&self, msg: String) -> anyhow::Result<()> {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::Publish { msg, resp_tx })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_notifications(&self) -> anyhow::Result<()> {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::ClearNotifications { resp_tx })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_read_until(&self, timestamp: u64) -> anyhow::Result<()> {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(SubscriptionRequest::UpdateReadUntil { timestamp, resp_tx })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
resp_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SubscriptionActor {
|
||||||
|
listener: ListenerHandle,
|
||||||
|
model: models::Subscription,
|
||||||
|
receiver: mpsc::Receiver<SubscriptionRequest>,
|
||||||
|
env: SharedEnv,
|
||||||
|
broadcast_tx: broadcast::Sender<ListenerEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubscriptionActor {
|
||||||
|
async fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
Ok(event) = self.listener.events.recv() => {
|
||||||
|
match event {
|
||||||
|
ListenerEvent::Message(msg) => self.handle_msg_event(msg),
|
||||||
|
other => {
|
||||||
|
let _ = self.broadcast_tx.send(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(request) = self.receiver.recv() => {
|
||||||
|
match request {
|
||||||
|
SubscriptionRequest::GetModel { resp_tx } => {
|
||||||
|
let _ = resp_tx.send(self.model.clone());
|
||||||
|
}
|
||||||
|
SubscriptionRequest::UpdateInfo {
|
||||||
|
mut new_model,
|
||||||
|
resp_tx,
|
||||||
|
} => {
|
||||||
|
new_model.server = self.model.server.clone();
|
||||||
|
new_model.topic = self.model.topic.clone();
|
||||||
|
let res = self.env.db.update_subscription(new_model.clone());
|
||||||
|
if let Ok(_) = res {
|
||||||
|
self.model = new_model;
|
||||||
|
}
|
||||||
|
resp_tx.send(res.map_err(|e| e.into()));
|
||||||
|
}
|
||||||
|
SubscriptionRequest::Publish {msg, resp_tx} => {
|
||||||
|
let _ = resp_tx.send(self.publish(msg).await);
|
||||||
|
}
|
||||||
|
SubscriptionRequest::Attach { resp_tx } => {
|
||||||
|
let messages = self
|
||||||
|
.env
|
||||||
|
.db
|
||||||
|
.list_messages(&self.model.server, &self.model.topic, 0)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut previous_events: Vec<ListenerEvent> = messages
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|msg| {
|
||||||
|
let msg = serde_json::from_str(&msg);
|
||||||
|
match msg {
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "error parsing stored message");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(msg) => Some(msg),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(ListenerEvent::Message)
|
||||||
|
.collect();
|
||||||
|
previous_events.push(ListenerEvent::ConnectionStateChanged(self.listener.request_state().await));
|
||||||
|
let _ = resp_tx.send((previous_events, self.broadcast_tx.subscribe()));
|
||||||
|
}
|
||||||
|
SubscriptionRequest::ClearNotifications {resp_tx} => {
|
||||||
|
let _ = resp_tx.send(self.env.db.delete_messages(&self.model.server, &self.model.topic).map_err(|e| anyhow::anyhow!(e)));
|
||||||
|
}
|
||||||
|
SubscriptionRequest::UpdateReadUntil { timestamp, resp_tx } => {
|
||||||
|
let res = self.env.db.update_read_until(&self.model.server, &self.model.topic, timestamp);
|
||||||
|
let _ = resp_tx.send(res.map_err(|e| anyhow::anyhow!(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publish(&self, msg: String) -> anyhow::Result<()> {
|
||||||
|
let server = &self.model.server;
|
||||||
|
let creds = self.env.credentials.get(server);
|
||||||
|
let mut req = self.env.http.post(server);
|
||||||
|
if let Some(creds) = creds {
|
||||||
|
req = req.basic_auth(creds.username, Some(creds.password));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("sending message");
|
||||||
|
let res = req.body(msg).send().await?;
|
||||||
|
res.error_for_status()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn handle_msg_event(&mut self, msg: Message) {
|
||||||
|
// Store in database
|
||||||
|
let already_stored: bool = {
|
||||||
|
let json_ev = &serde_json::to_string(&msg).unwrap();
|
||||||
|
match self.env.db.insert_message(&self.model.server, json_ev) {
|
||||||
|
Err(Error::DuplicateMessage) => {
|
||||||
|
warn!("Received duplicate message");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "Can't store the message");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !already_stored {
|
||||||
|
// Show notification. If this fails, panic
|
||||||
|
if !{ self.model.muted } {
|
||||||
|
let notifier = self.env.proxy.clone();
|
||||||
|
|
||||||
|
let title = { msg.notification_title(&self.model) };
|
||||||
|
|
||||||
|
let n = models::Notification {
|
||||||
|
title,
|
||||||
|
body: msg.display_message().as_deref().unwrap_or("").to_string(),
|
||||||
|
actions: msg.actions.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Showing notification");
|
||||||
|
notifier.send(n).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to app
|
||||||
|
let _ = self.broadcast_tx.send(ListenerEvent::Message(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionRequest {
|
||||||
|
GetModel {
|
||||||
|
resp_tx: oneshot::Sender<models::Subscription>,
|
||||||
|
},
|
||||||
|
UpdateInfo {
|
||||||
|
new_model: models::Subscription,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
Attach {
|
||||||
|
resp_tx: oneshot::Sender<(Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>)>,
|
||||||
|
},
|
||||||
|
Publish {
|
||||||
|
msg: String,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
ClearNotifications {
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
UpdateReadUntil {
|
||||||
|
timestamp: u64,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ use gio::UnixSocketAddress;
|
|||||||
use gtk::{gdk, gio, glib};
|
use gtk::{gdk, gio, glib};
|
||||||
use ntfy_daemon::models;
|
use ntfy_daemon::models;
|
||||||
use ntfy_daemon::ntfy_capnp::system_notifier;
|
use ntfy_daemon::ntfy_capnp::system_notifier;
|
||||||
|
use ntfy_daemon::NtfyHandle;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION};
|
use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION};
|
||||||
@ -23,7 +24,6 @@ mod imp {
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use glib::WeakRef;
|
use glib::WeakRef;
|
||||||
use ntfy_daemon::Ntfy;
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -33,7 +33,7 @@ mod imp {
|
|||||||
pub window: RefCell<WeakRef<NotifyWindow>>,
|
pub window: RefCell<WeakRef<NotifyWindow>>,
|
||||||
pub socket_path: RefCell<PathBuf>,
|
pub socket_path: RefCell<PathBuf>,
|
||||||
pub hold_guard: OnceCell<gio::ApplicationHoldGuard>,
|
pub hold_guard: OnceCell<gio::ApplicationHoldGuard>,
|
||||||
pub ntfy: OnceCell<Ntfy>,
|
pub ntfy: OnceCell<NtfyHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@ -319,7 +319,7 @@ impl NotifyApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let proxies = std::sync::Arc::new(Proxies { notification: s });
|
let proxies = std::sync::Arc::new(Proxies { notification: s });
|
||||||
let ntfy = ntfy_daemon::Ntfy::start(
|
let ntfy = ntfy_daemon::start(
|
||||||
socket_path.to_owned(),
|
socket_path.to_owned(),
|
||||||
dbpath.to_str().unwrap(),
|
dbpath.to_str().unwrap(),
|
||||||
proxies.clone(),
|
proxies.clone(),
|
||||||
|
|||||||
@ -4,53 +4,37 @@ use std::rc::Rc;
|
|||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use capnp::capability::Promise;
|
use capnp::capability::Promise;
|
||||||
use capnp_rpc::pry;
|
use capnp_rpc::pry;
|
||||||
|
use futures::join;
|
||||||
use glib::subclass::prelude::*;
|
use glib::subclass::prelude::*;
|
||||||
use glib::Properties;
|
use glib::Properties;
|
||||||
|
use gtk::glib::MainContext;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
use ntfy_daemon::models;
|
use ntfy_daemon::ntfy_capnp::{output_channel, subscription, watch_handle};
|
||||||
use ntfy_daemon::ntfy_capnp::{output_channel, subscription, watch_handle, Status};
|
use ntfy_daemon::{models, ConnectionState, ListenerEvent};
|
||||||
use tracing::{debug, error, instrument};
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
struct TopicWatcher {
|
#[repr(u16)]
|
||||||
sub: glib::WeakRef<Subscription>,
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
Down = 0,
|
||||||
|
Degraded = 1,
|
||||||
|
Up = 2,
|
||||||
}
|
}
|
||||||
impl output_channel::Server for TopicWatcher {
|
|
||||||
fn send_message(
|
|
||||||
&mut self,
|
|
||||||
params: output_channel::SendMessageParams,
|
|
||||||
_results: output_channel::SendMessageResults,
|
|
||||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
|
||||||
if let Some(sub) = self.sub.upgrade() {
|
|
||||||
let request = pry!(params.get());
|
|
||||||
let message = pry!(pry!(request.get_message()).to_str());
|
|
||||||
|
|
||||||
let msg: models::Message = serde_json::from_str(message).unwrap();
|
impl From<u16> for Status {
|
||||||
sub.imp().messages.append(&glib::BoxedAnyObject::new(msg));
|
fn from(value: u16) -> Self {
|
||||||
sub.update_unread_count();
|
match value {
|
||||||
Promise::ok(())
|
0 => Status::Down,
|
||||||
} else {
|
1 => Status::Degraded,
|
||||||
Promise::err(capnp::Error::failed("dead channel".to_string()))
|
2 => Status::Up,
|
||||||
}
|
_ => panic!("Invalid value for Status"),
|
||||||
}
|
|
||||||
fn send_status(
|
|
||||||
&mut self,
|
|
||||||
params: output_channel::SendStatusParams,
|
|
||||||
_: output_channel::SendStatusResults,
|
|
||||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
|
||||||
if let Some(sub) = self.sub.upgrade() {
|
|
||||||
let status = pry!(pry!(params.get()).get_status());
|
|
||||||
sub.imp().status.set(status);
|
|
||||||
sub.notify_status();
|
|
||||||
Promise::ok(())
|
|
||||||
} else {
|
|
||||||
Promise::err(capnp::Error::failed("dead channel".to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TopicWatcher {
|
impl From<Status> for u16 {
|
||||||
fn drop(&mut self) {
|
fn from(status: Status) -> Self {
|
||||||
debug!("Dropped topic watcher");
|
status as u16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +60,7 @@ mod imp {
|
|||||||
pub unread_count: Cell<u32>,
|
pub unread_count: Cell<u32>,
|
||||||
pub read_until: Cell<u64>,
|
pub read_until: Cell<u64>,
|
||||||
pub messages: gio::ListStore,
|
pub messages: gio::ListStore,
|
||||||
pub client: OnceCell<subscription::Client>,
|
pub client: OnceCell<ntfy_daemon::SubscriptionHandle>,
|
||||||
pub remote_handle: RefCell<Option<watch_handle::Client>>,
|
pub remote_handle: RefCell<Option<watch_handle::Client>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +104,7 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Subscription {
|
impl Subscription {
|
||||||
pub fn new(client: subscription::Client) -> Self {
|
pub fn new(client: ntfy_daemon::SubscriptionHandle) -> Self {
|
||||||
let this: Self = glib::Object::builder().build();
|
let this: Self = glib::Object::builder().build();
|
||||||
let imp = this.imp();
|
let imp = this.imp();
|
||||||
if let Err(_) = imp.client.set(client) {
|
if let Err(_) = imp.client.set(client) {
|
||||||
@ -161,34 +145,56 @@ impl Subscription {
|
|||||||
|
|
||||||
fn load(&self) -> Promise<(), capnp::Error> {
|
fn load(&self) -> Promise<(), capnp::Error> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let req_info = imp.client.get().unwrap().get_info_request();
|
|
||||||
let req_messages = {
|
|
||||||
let mut req = imp.client.get().unwrap().watch_request();
|
|
||||||
req.get().set_watcher(capnp_rpc::new_client(TopicWatcher {
|
|
||||||
sub: self.downgrade(),
|
|
||||||
}));
|
|
||||||
req
|
|
||||||
};
|
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
Promise::from_future(async move {
|
Promise::from_future(async move {
|
||||||
let info = req_info.send().promise.await?;
|
let remote_subscription = this.imp().client.get().unwrap();
|
||||||
let info = info.get()?;
|
let model = remote_subscription.model().await;
|
||||||
|
|
||||||
this.init_info(
|
this.init_info(
|
||||||
info.get_topic()?.to_str()?,
|
&model.topic,
|
||||||
info.get_server()?.to_str()?,
|
&model.server,
|
||||||
info.get_muted(),
|
model.muted,
|
||||||
info.get_read_until(),
|
model.read_until,
|
||||||
info.get_display_name()?.to_str()?,
|
&model.display_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
let message_stream = req_messages.send().promise.await?;
|
let (prev_msgs, mut rx) = remote_subscription.attach().await;
|
||||||
let handle = message_stream.get()?.get_handle()?;
|
|
||||||
this.imp().remote_handle.replace(Some(handle));
|
for msg in prev_msgs {
|
||||||
|
this.handle_event(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Ok(ev) = rx.recv().await {
|
||||||
|
this.handle_event(ev);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_event(&self, ev: ListenerEvent) {
|
||||||
|
match dbg!(ev) {
|
||||||
|
ListenerEvent::Message(msg) => {
|
||||||
|
self.imp().messages.append(&glib::BoxedAnyObject::new(msg));
|
||||||
|
self.update_unread_count();
|
||||||
|
}
|
||||||
|
ListenerEvent::ConnectionStateChanged(connection_state) => {
|
||||||
|
self.set_connection_state(connection_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_connection_state(&self, state: ConnectionState) {
|
||||||
|
let status = match state {
|
||||||
|
ConnectionState::Unitialized => Status::Degraded,
|
||||||
|
ConnectionState::Connected => Status::Up,
|
||||||
|
ConnectionState::Reconnecting { .. } => Status::Degraded,
|
||||||
|
};
|
||||||
|
self.imp().status.set(status);
|
||||||
|
dbg!(status);
|
||||||
|
self.notify_status();
|
||||||
|
}
|
||||||
|
|
||||||
fn _set_display_name(&self, value: String) {
|
fn _set_display_name(&self, value: String) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let value = if value.is_empty() {
|
let value = if value.is_empty() {
|
||||||
@ -209,18 +215,15 @@ impl Subscription {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_updated_info(&self) -> Promise<(), anyhow::Error> {
|
async fn send_updated_info(&self) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let mut req = imp.client.get().unwrap().update_info_request();
|
imp.client.get().unwrap().update_info(
|
||||||
let mut val = pry!(req.get().get_value());
|
models::Subscription::builder(self.topic())
|
||||||
val.set_muted(imp.muted.get());
|
.display_name((imp.display_name.borrow().to_string()))
|
||||||
val.set_display_name(imp.display_name.borrow().as_str().into());
|
.muted(imp.muted.get())
|
||||||
val.set_read_until(imp.read_until.get());
|
.build().map_err(|e| anyhow::anyhow!("invalid subscription data"))?
|
||||||
Promise::from_future(async move {
|
).await?;
|
||||||
debug!("sending update_info");
|
Ok(())
|
||||||
req.send().promise.await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
fn last_message(list: &gio::ListStore) -> Option<models::Message> {
|
fn last_message(list: &gio::ListStore) -> Option<models::Message> {
|
||||||
let n = list.n_items();
|
let n = list.n_items();
|
||||||
@ -249,51 +252,38 @@ impl Subscription {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn flag_all_as_read(&self) -> Promise<(), anyhow::Error> {
|
pub async fn flag_all_as_read(&self) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let Some(value) = Self::last_message(&imp.messages)
|
let Some(value) = Self::last_message(&imp.messages)
|
||||||
.map(|last| last.time)
|
.map(|last| last.time)
|
||||||
.filter(|time| *time > self.imp().read_until.get())
|
.filter(|time| *time > self.imp().read_until.get())
|
||||||
else {
|
else {
|
||||||
return Promise::ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
Promise::from_future(async move {
|
this.imp().client.get().unwrap().update_read_until(value).await?;
|
||||||
let mut req = this.imp().client.get().unwrap().update_read_until_request();
|
this.imp().read_until.set(value);
|
||||||
req.get().set_value(value);
|
this.update_unread_count();
|
||||||
req.send().promise.await?;
|
|
||||||
this.imp().read_until.set(value);
|
Ok(())
|
||||||
this.update_unread_count();
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
pub fn publish_msg(&self, mut msg: models::Message) -> Promise<(), anyhow::Error> {
|
pub async fn publish_msg(&self, mut msg: models::Message) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let json = {
|
let json = {
|
||||||
msg.topic = self.topic();
|
msg.topic = self.topic();
|
||||||
serde_json::to_string(&msg)
|
serde_json::to_string(&msg)?
|
||||||
};
|
};
|
||||||
let mut req = imp.client.get().unwrap().publish_request();
|
imp.client.get().unwrap().publish(json).await?;
|
||||||
req.get().set_message(pry!(json).as_str().into());
|
Ok(())
|
||||||
|
|
||||||
Promise::from_future(async move {
|
|
||||||
debug!("sending publish");
|
|
||||||
req.send().promise.await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn clear_notifications(&self) -> Promise<(), anyhow::Error> {
|
pub async fn clear_notifications(&self) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let req = imp.client.get().unwrap().clear_notifications_request();
|
imp.client.get().unwrap().clear_notifications().await?;
|
||||||
let this = self.clone();
|
self.imp().messages.remove_all();
|
||||||
Promise::from_future(async move {
|
|
||||||
debug!("sending clear_notifications");
|
Ok(())
|
||||||
req.send().promise.await?;
|
|
||||||
this.imp().messages.remove_all();
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nice_status(&self) -> Status {
|
pub fn nice_status(&self) -> Status {
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
use ntfy_daemon::models;
|
use ntfy_daemon::models;
|
||||||
use ntfy_daemon::ntfy_capnp::{system_notifier, Status};
|
use ntfy_daemon::ntfy_capnp::system_notifier;
|
||||||
use ntfy_daemon::Ntfy;
|
use ntfy_daemon::NtfyHandle;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::application::NotifyApplication;
|
use crate::application::NotifyApplication;
|
||||||
use crate::config::{APP_ID, PROFILE};
|
use crate::config::{APP_ID, PROFILE};
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
|
use crate::subscription::Status;
|
||||||
use crate::subscription::Subscription;
|
use crate::subscription::Subscription;
|
||||||
use crate::widgets::*;
|
use crate::widgets::*;
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ mod imp {
|
|||||||
pub send_btn: TemplateChild<gtk::Button>,
|
pub send_btn: TemplateChild<gtk::Button>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub code_btn: TemplateChild<gtk::Button>,
|
pub code_btn: TemplateChild<gtk::Button>,
|
||||||
pub notifier: OnceCell<Ntfy>,
|
pub notifier: OnceCell<NtfyHandle>,
|
||||||
pub conn: OnceCell<gio::SocketConnection>,
|
pub conn: OnceCell<gio::SocketConnection>,
|
||||||
pub settings: gio::Settings,
|
pub settings: gio::Settings,
|
||||||
pub banner_binding: Cell<Option<(Subscription, glib::SignalHandlerId)>>,
|
pub banner_binding: Cell<Option<(Subscription, glib::SignalHandlerId)>>,
|
||||||
@ -139,7 +141,7 @@ mod imp {
|
|||||||
});
|
});
|
||||||
klass.install_action("win.clear-notifications", None, |this, _, _| {
|
klass.install_action("win.clear-notifications", None, |this, _, _| {
|
||||||
this.selected_subscription().map(|sub| {
|
this.selected_subscription().map(|sub| {
|
||||||
this.error_boundary().spawn(sub.clear_notifications());
|
this.error_boundary().spawn(async move {sub.clear_notifications().await});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
//klass.bind_template_instance_callbacks();
|
//klass.bind_template_instance_callbacks();
|
||||||
@ -191,7 +193,7 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NotifyWindow {
|
impl NotifyWindow {
|
||||||
pub fn new(app: &NotifyApplication, notifier: Ntfy) -> Self {
|
pub fn new(app: &NotifyApplication, notifier: NtfyHandle) -> Self {
|
||||||
let obj: Self = glib::Object::builder().property("application", app).build();
|
let obj: Self = glib::Object::builder().property("application", app).build();
|
||||||
|
|
||||||
if let Err(_) = obj.imp().notifier.set(notifier) {
|
if let Err(_) = obj.imp().notifier.set(notifier) {
|
||||||
@ -212,24 +214,25 @@ impl NotifyWindow {
|
|||||||
fn connect_entry_and_send_btn(&self) {
|
fn connect_entry_and_send_btn(&self) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
let entry = imp.entry.clone();
|
|
||||||
let publish = move || {
|
imp.entry.connect_activate(move |_| this.publish_msg());
|
||||||
let p = this
|
let this = self.clone();
|
||||||
.selected_subscription()
|
imp.send_btn.connect_clicked(move |_| this.publish_msg());
|
||||||
|
}
|
||||||
|
fn publish_msg(&self) {
|
||||||
|
let entry = self.imp().entry.clone();
|
||||||
|
let this = self.clone();
|
||||||
|
|
||||||
|
entry.error_boundary().spawn(async move {
|
||||||
|
this.selected_subscription()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.publish_msg(models::Message {
|
.publish_msg(models::Message {
|
||||||
message: Some(entry.text().as_str().to_string()),
|
message: Some(entry.text().as_str().to_string()),
|
||||||
..models::Message::default()
|
..models::Message::default()
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
entry.error_boundary().spawn(async move {
|
Ok(())
|
||||||
p.await?;
|
});
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let publishc = publish.clone();
|
|
||||||
imp.entry.connect_activate(move |_| publishc());
|
|
||||||
imp.send_btn.connect_clicked(move |_| publish());
|
|
||||||
}
|
}
|
||||||
fn connect_code_btn(&self) {
|
fn connect_code_btn(&self) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
@ -261,26 +264,27 @@ impl NotifyWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_subscription(&self, sub: models::Subscription) {
|
fn add_subscription(&self, sub: models::Subscription) {
|
||||||
// let mut req = self.notifier().subscribe_request();
|
let this = self.clone();
|
||||||
|
self.error_boundary().spawn(async move {
|
||||||
|
let sub = this
|
||||||
|
.notifier()
|
||||||
|
.subscribe(&sub.server, &sub.topic)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
anyhow::anyhow!(err.into_iter().map(|x| x.to_string()).collect::<String>())
|
||||||
|
})?;
|
||||||
|
let imp = this.imp();
|
||||||
|
|
||||||
// req.get().set_server(sub.server.as_str().into());
|
// Subscription::new will use the pipelined client to retrieve info about the subscription
|
||||||
// req.get().set_topic(sub.topic.as_str().into());
|
let subscription = Subscription::new(sub);
|
||||||
// let res = req.send();
|
// We want to still check if there were any errors adding the subscription.
|
||||||
// let this = self.clone();
|
|
||||||
// self.error_boundary().spawn(async move {
|
|
||||||
// let imp = this.imp();
|
|
||||||
|
|
||||||
// // Subscription::new will use the pipelined client to retrieve info about the subscription
|
imp.subscription_list_model.append(&subscription);
|
||||||
// let subscription = Subscription::new(res.pipeline.get_subscription());
|
let i = imp.subscription_list_model.n_items() - 1;
|
||||||
// // We want to still check if there were any errors adding the subscription.
|
let row = imp.subscription_list.row_at_index(i as i32);
|
||||||
// res.promise.await?;
|
imp.subscription_list.select_row(row.as_ref());
|
||||||
|
Ok(())
|
||||||
// imp.subscription_list_model.append(&subscription);
|
});
|
||||||
// let i = imp.subscription_list_model.n_items() - 1;
|
|
||||||
// let row = imp.subscription_list.row_at_index(i as i32);
|
|
||||||
// imp.subscription_list.select_row(row.as_ref());
|
|
||||||
// Ok(())
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unsubscribe(&self) {
|
fn unsubscribe(&self) {
|
||||||
@ -303,7 +307,7 @@ impl NotifyWindow {
|
|||||||
// Ok(())
|
// Ok(())
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
fn notifier(&self) -> &Ntfy {
|
fn notifier(&self) -> &NtfyHandle {
|
||||||
self.imp().notifier.get().unwrap()
|
self.imp().notifier.get().unwrap()
|
||||||
}
|
}
|
||||||
fn selected_subscription(&self) -> Option<Subscription> {
|
fn selected_subscription(&self) -> Option<Subscription> {
|
||||||
@ -314,32 +318,31 @@ impl NotifyWindow {
|
|||||||
.and_downcast::<Subscription>()
|
.and_downcast::<Subscription>()
|
||||||
}
|
}
|
||||||
fn bind_message_list(&self) {
|
fn bind_message_list(&self) {
|
||||||
// let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
|
||||||
// imp.subscription_list
|
imp.subscription_list
|
||||||
// .bind_model(Some(&imp.subscription_list_model), |obj| {
|
.bind_model(Some(&imp.subscription_list_model), |obj| {
|
||||||
// let sub = obj.downcast_ref::<Subscription>().unwrap();
|
let sub = obj.downcast_ref::<Subscription>().unwrap();
|
||||||
|
|
||||||
// Self::build_subscription_row(&sub).upcast()
|
Self::build_subscription_row(&sub).upcast()
|
||||||
// });
|
});
|
||||||
|
|
||||||
// let this = self.clone();
|
let this = self.clone();
|
||||||
// imp.subscription_list.connect_row_selected(move |_, _row| {
|
imp.subscription_list.connect_row_selected(move |_, _row| {
|
||||||
// this.selected_subscription_changed(this.selected_subscription().as_ref());
|
this.selected_subscription_changed(this.selected_subscription().as_ref());
|
||||||
// });
|
});
|
||||||
|
|
||||||
// let this = self.clone();
|
let this = self.clone();
|
||||||
// let req = self.notifier().list_subscriptions_request();
|
self.error_boundary().spawn(async move {
|
||||||
// let res = req.send();
|
glib::timeout_future_seconds(1).await;
|
||||||
// self.error_boundary().spawn(async move {
|
let list = this.notifier().list_subscriptions().await?;
|
||||||
// let list = res.promise.await?;
|
for sub in list {
|
||||||
// let list = list.get()?.get_list()?;
|
this.imp()
|
||||||
// let imp = this.imp();
|
.subscription_list_model
|
||||||
// for sub in list {
|
.append(&Subscription::new(sub));
|
||||||
// imp.subscription_list_model.append(&Subscription::new(sub?));
|
}
|
||||||
// }
|
Ok(())
|
||||||
// Ok(())
|
});
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
fn update_banner(&self, sub: Option<&Subscription>) {
|
fn update_banner(&self, sub: Option<&Subscription>) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
@ -403,7 +406,7 @@ impl NotifyWindow {
|
|||||||
{
|
{
|
||||||
self.selected_subscription().map(|sub| {
|
self.selected_subscription().map(|sub| {
|
||||||
self.error_boundary()
|
self.error_boundary()
|
||||||
.spawn(sub.flag_all_as_read().map_err(|e| e.into()));
|
.spawn(async move {sub.flag_all_as_read().await});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user