pub mod formats { pub mod json; pub mod plain; pub mod rson; pub mod serde; } mod notification; use notification::{to_notif, Notification}; use std::collections::HashMap; pub mod optparse; use futures_util::stream::TryStreamExt; use zbus::{message::Body, Connection, Result}; const SERVER_NAME: &str = "SNot"; // Server software name const VENDOR: &str = "candifloss.cc"; // Server software vendor const VERSION: &str = env!("CARGO_PKG_VERSION"); // Server software version, from Cargo.toml const SPEC_VERSION: &str = "0.42"; // DBus specification version const NOTIF_INTERFACE: &str = "org.freedesktop.Notifications"; // DBus interface name // Properties of this server fn server_properties() -> HashMap { let mut properties: HashMap = HashMap::new(); properties.insert("ServerName".to_string(), String::from(SERVER_NAME)); properties.insert("Vendor".to_string(), String::from(VENDOR)); properties.insert("Version".to_string(), String::from(VERSION)); properties.insert("SpecVersion".to_string(), String::from(SPEC_VERSION)); properties } #[tokio::main] async fn main() -> Result<()> { // Options let args: optparse::Cli = argh::from_env(); // Format: r|j|p let op_format = match args.format { Some(value) => { // Reject invalid format option if ["r", "j", "p"].contains(&value.as_str()) { value.clone() } else { // Exit with error code return Err(zbus::Error::from(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Invalid output format", ))); } } None => "j".to_string(), }; // Verbose mode. Off/false by default let verbose = args.verbose; let connection = Connection::session().await?; connection .request_name(NOTIF_INTERFACE) // Requesting dbus for this service name. Any other services/procs using this name should be stopped/disabled before this .await?; let mut stream = zbus::MessageStream::from(&connection); // Convert connection to a MessageStream, yields Message items // Notification id, restarts with each session let mut notification_id: u32 = 0; // Iterate on the message stream while let Some(msg) = stream.try_next().await? { // Check the method calls in the received message's header if let Some(member) = msg.header().member() { let member_name = member.as_str(); // Convert to &str for the match match member_name { "GetAll" => { // Client requested all properties of the server. Extract the interface name from the message body let interface_name: String = msg.body().deserialize()?; // This method has only one parameter, the interface name // Check if the requested interface is the right one if interface_name == NOTIF_INTERFACE { // Properties for the interface let properties = server_properties(); // Reply with the properties connection.reply(&msg, &properties).await?; if verbose { println!("GetAll request received for interface: {interface_name}"); } } else { // Reply with an error connection .reply_error( &msg, "org.freedesktop.DBus.Error.UnknownInterface", &"Unknown interface".to_string(), ) .await?; if verbose { println!("Unknown interface requested: {interface_name}"); } } } "GetServerInformation" => { // Client requested server information. Respond with: (Server_name, author, software_version, dbus_spec_version) let response = (SERVER_NAME, VENDOR, VERSION, SPEC_VERSION); connection.reply(&msg, &response).await?; if verbose { println!("Request received: {member}\n\tName: {SERVER_NAME}, Vendor: {VENDOR}, Version: {VERSION}, DBus spec version: {SPEC_VERSION}"); // Remove this LATER } } "GetCapabilities" => { // Client requested server capabilities. Respond with the supported capabilities let capabilities = vec!["actions", "body", "body-hyperlinks"]; // Add more LATER connection.reply(&msg, &capabilities).await?; if verbose { println!("Request received: {member}\n\tCapabilities: {capabilities:?}"); // Remove this LATER } } "Notify" => { // New notification received. Now, respond to the client with a notification ID notification_id += 1; // This could be incremented or generated. connection.reply(&msg, ¬ification_id).await?; // The client waits for this response in order to disconnect // Get the body of the message let msg_body: Body = msg.body(); // The body has 8 parameters, ie., 8 fields of a Notification object // Convert the msg body to a Notification object let notif: Notification = to_notif(&msg_body)?; // Handle the notif match op_format.as_str() { "j" => { println!("{}", ¬if.json()); // Print the json version } "r" => { // Print the rson version match notif.rson() { Ok(rson_string) => println!("{}", rson_string), Err(e) => eprintln!("Failed to convert to RSON: {}", e), } } "p" => { println!("{}\n", ¬if.plain()); // Print the plain version } _ => { println!("Onkown output format."); // This is probably unreachable } } } "CloseNotification" => { // Client sent a 'close' signal. Extract notification ID of the notif to be closed from the message body let notification_id: u32 = msg.body().deserialize()?; // This method has only one parameter, the id // Tracking notifications by their IDs, closing them, and other features may be implemented later // close_notification(notification_id); if verbose { println!("Closing notification with ID: {notification_id}"); } // Respond to the client, acknowledging the closure connection.reply(&msg, &()).await?; } _ => { if verbose { println!("Unhandled method: {member}"); // Other methods are either irrelevant or unhandled at this stage of development } } } } } Ok(()) }