done, for now
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
config.toml
|
||||
2382
Cargo.lock
generated
Normal file
2382
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "notification-pusher-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rumqttc = "0.19" # MQTT client
|
||||
tokio = { version = "1", features = ["full"] } # async runtime
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.7"
|
||||
reqwest = { version = "0.12.23", features = ["json", "rustls-tls"] }
|
||||
log = "0.4"
|
||||
tokio-stream = "0.1"
|
||||
simple_logger = "5.0.0"
|
||||
188
src/main.rs
Normal file
188
src/main.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use rumqttc::{AsyncClient, Event, MqttOptions, QoS};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, process, thread};
|
||||
use tokio::time::timeout;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ConfigBroker {
|
||||
address: String,
|
||||
client_id: String,
|
||||
topic: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ConfigAuth {
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Config {
|
||||
broker: ConfigBroker,
|
||||
auth: ConfigAuth,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Notification {
|
||||
title: String,
|
||||
body: String,
|
||||
topic: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init().unwrap();
|
||||
|
||||
// Load config.toml
|
||||
let config_content = fs::read_to_string("config.toml")
|
||||
.expect("Failed to read config.toml");
|
||||
|
||||
let config: Config = toml::from_str(&config_content)
|
||||
.expect("Failed to parse config.toml");
|
||||
|
||||
if config.broker.address.is_empty()
|
||||
|| config.broker.client_id.is_empty()
|
||||
|| config.broker.topic.is_empty()
|
||||
{
|
||||
log::error!("Missing broker configuration in config.toml");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let mut mqttoptions = MqttOptions::new(
|
||||
format!("{}-0", config.broker.client_id),
|
||||
&config.broker.address,
|
||||
1883,
|
||||
);
|
||||
|
||||
mqttoptions.set_clean_session(true);
|
||||
if let Some(username) = &config.auth.username {
|
||||
mqttoptions.set_credentials(username, config.auth.password.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
|
||||
|
||||
log::info!(
|
||||
"Connecting to broker at {} with client ID '{}'",
|
||||
&config.broker.address,
|
||||
&config.broker.client_id
|
||||
);
|
||||
|
||||
// Connect is implicit when eventloop.poll() is called for the first time
|
||||
|
||||
// Subscribe to topic
|
||||
client
|
||||
.subscribe(&config.broker.topic, QoS::AtLeastOnce)
|
||||
.await
|
||||
.expect("Failed to subscribe");
|
||||
|
||||
log::info!("Subscribed to topic '{}'", &config.broker.topic);
|
||||
|
||||
// Publish test message
|
||||
let test_message = format!("Hello from {}", config.broker.client_id);
|
||||
client
|
||||
.publish(
|
||||
&config.broker.topic,
|
||||
QoS::AtLeastOnce,
|
||||
false,
|
||||
test_message.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to publish message");
|
||||
|
||||
log::info!("Published message: {}", test_message);
|
||||
|
||||
// Wait for one message
|
||||
log::info!("Waiting for incoming message...");
|
||||
let mut received_message = None;
|
||||
|
||||
// We run event loop with timeout to consume one message
|
||||
let timeout_duration = Duration::from_secs(5);
|
||||
let start = Instant::now();
|
||||
|
||||
while start.elapsed() < timeout_duration {
|
||||
match timeout(Duration::from_millis(100), eventloop.poll()).await {
|
||||
Ok(Ok(event)) => {
|
||||
if let Event::Incoming(rumqttc::Packet::Publish(publish)) = event {
|
||||
let payload = String::from_utf8_lossy(&publish.payload);
|
||||
log::info!(
|
||||
"Received message on topic '{}': {}",
|
||||
publish.topic,
|
||||
payload
|
||||
);
|
||||
received_message = Some(payload.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("MQTT eventloop error: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
// timeout on poll, loop again
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if received_message.is_none() {
|
||||
log::warn!("No message received.");
|
||||
}
|
||||
|
||||
// Start notification daemon (listen indefinitely)
|
||||
log::info!("Starting notification daemon");
|
||||
|
||||
start_notification_daemon(client, eventloop, config.broker.topic).await;
|
||||
}
|
||||
|
||||
async fn start_notification_daemon(
|
||||
client: AsyncClient,
|
||||
mut eventloop: rumqttc::EventLoop,
|
||||
topic: String,
|
||||
) {
|
||||
client
|
||||
.subscribe(&topic, QoS::AtLeastOnce)
|
||||
.await
|
||||
.expect("Failed to subscribe in daemon");
|
||||
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(rumqttc::Event::Incoming(rumqttc::Packet::Publish(publish))) => {
|
||||
let payload = String::from_utf8_lossy(&publish.payload);
|
||||
log::debug!("Received MQTT message: {} on topic {}", payload, publish.topic);
|
||||
|
||||
match serde_json::from_str::<Notification>(&payload) {
|
||||
Ok(notif) => {
|
||||
// Send to ntfy.sh
|
||||
let url = format!("https://ntfy.sh/{}", notif.topic);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
match client
|
||||
.post(&url)
|
||||
.header("Title", ¬if.title)
|
||||
.body(notif.body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
log::debug!("ntfy.sh status code: {}", resp.status());
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to send notification: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to parse JSON message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("MQTT eventloop error in daemon: {}", e);
|
||||
// Optionally reconnect here if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user