53 Commits

Author SHA1 Message Date
22e32a5715 working on tilde integration 2025-02-19 14:43:31 -05:00
6bbfb454de working on tild integration 2025-02-19 13:39:17 -05:00
b4e2dd2683 a lot of things are working 2024-09-09 15:34:22 -04:00
2c26200867 remove golgi dependency 2024-09-08 14:45:40 -04:00
6cc8faa0c3 Merge pull request 'Reintroduce status and power-related templates and routes' (#140) from refactor_stats into main
Reviewed-on: #140
2022-11-28 07:18:00 +00:00
6028e07bde single variable name change for clarity 2022-11-28 09:12:25 +02:00
ebc7b9d417 remove old context code and refine status parsing 2022-11-28 09:12:01 +02:00
b8ff944377 conditionally render status url based on run-mode 2022-11-28 09:11:21 +02:00
8cbb295c3a add power menu to settings menu and mount routes 2022-11-28 09:10:42 +02:00
7d5d6bcc1f add power menu template builder and mount route 2022-11-03 12:02:01 +00:00
8c3a92aa88 update lockfile 2022-11-02 15:16:21 +00:00
cfe270a995 mount device status route 2022-11-02 15:16:00 +00:00
2eca779208 add peach-stats dependency 2022-11-02 15:15:42 +00:00
a1b16f8d38 add refactored device status template and import module 2022-11-02 15:15:06 +00:00
3bf095e148 bump the version number and update the lockfile 2022-10-25 15:16:17 +01:00
d9167a2cd6 mount the network status route 2022-10-25 15:15:13 +01:00
4e7fbd5fdf add the refactored template for network status 2022-10-25 15:14:52 +01:00
0fab57d94f uncomment vnstat_parse dependency 2022-10-25 15:14:01 +01:00
441d2a6a3b Merge pull request 'Specify network iface values as consts' (#139) from iface_config_vars into main
Reviewed-on: #139
2022-10-18 15:02:36 +00:00
52e0aff4d1 bump version 2022-10-18 15:58:24 +01:00
24ceedbb9d replace scattered values for wlan0 and ap0 with const values 2022-10-18 15:57:40 +01:00
d3ab490c05 Merge pull request 'Reintroduce networking-related templates and routes' (#138) from system_mode into main
Reviewed-on: #138
2022-10-18 11:14:20 +00:00
1e7a54b728 remove blank line in template 2022-10-18 12:06:36 +01:00
3eab3e3687 add and mount ap detail template 2022-10-18 12:01:28 +01:00
8b0381ead1 fix template indentation 2022-10-18 12:00:40 +01:00
e91c40355a add template builder and form handler for adding wifi ap 2022-10-10 10:39:29 +01:00
8cd8ee5dd6 mount routes for adding wifi ap credentials 2022-10-10 10:39:04 +01:00
24deb4601a update lockfile 2022-10-10 09:18:20 +01:00
fedf2855ed add data usage template module but leave it commented out for now 2022-10-10 09:17:54 +01:00
0814eedf13 add ap list template and mount route 2022-10-03 11:40:26 +01:00
4fb4ea2f9c merge latest lockfile 2022-10-03 11:39:58 +01:00
8e283fbc6e merge upstream network api changes 2022-10-03 11:39:28 +01:00
bdd3b7ab9b add wip refactored template for ap detail 2022-10-03 10:48:56 +01:00
4f36f61128 add refactored template for ap list 2022-10-03 10:48:25 +01:00
acab30acce mount GET and POST routes for dns configuration 2022-09-30 15:34:19 +01:00
61ef909ed3 add dns configuration template builder and form handler 2022-09-30 15:33:36 +01:00
97030fbfbf mount GET and POST routes for modifying wifi ap password 2022-09-29 14:27:46 +01:00
b6cd54142c add template builder and form handler for modifying wifi ap password 2022-09-29 14:26:56 +01:00
67f33385e5 Merge pull request 'Add method to return list of all saved and in-range access points for a given interface' (#137) from list_networks into main
Reviewed-on: #137
2022-09-27 13:57:22 +00:00
a9bcc267a2 bump minor version 2022-09-27 14:52:13 +01:00
a513b7aa5b add method to return list of all saved and in-range access points for the given interface 2022-09-27 14:51:09 +01:00
1a7bd7987b add network settings menu template and route handler, along with network settings placeholder files for routes 2022-09-26 16:44:22 +01:00
c5c0bb91e4 Merge pull request 'Update lockfile with kuska and golgi crate updates' (#136) from update_lockfile into main
Reviewed-on: #136
2022-09-26 09:27:14 +00:00
5a50730435 update lockfile with kuska and golgi crate updates 2022-09-26 10:24:07 +01:00
86b4714274 Merge pull request 'Update create_history_stream to take args struct' (#135) from history_stream_args into main
Reviewed-on: #135
2022-09-26 09:18:32 +00:00
d5a2390e29 update create history stream to take args struct 2022-09-26 10:14:47 +01:00
c83a22461d Merge pull request 'Add wait-for-sbot to peach-config' (#131) from wait-for-sbot into main
Reviewed-on: #131
2022-07-25 11:17:31 +00:00
40bd1e48f1 Merge branch 'main' into wait-for-sbot
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-25 10:41:20 +00:00
03ac890793 Cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-15 11:37:05 +02:00
bc0c0fca7f Sequential match statements
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-15 11:35:28 +02:00
fc50bb5ee5 Cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-12 12:29:47 +02:00
29f5ad0e84 Wait for sbot is working
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-12 12:18:54 +02:00
cb09d6c3e9 Wait for sbot 2022-07-12 11:51:49 +02:00
49 changed files with 3274 additions and 1179 deletions

824
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ members = [
"peach-monitor", "peach-monitor",
"peach-stats", "peach-stats",
"peach-jsonrpc-server", "peach-jsonrpc-server",
"peach-dyndns-updater" "peach-dyndns-updater",
"tilde-client"
] ]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-config" name = "peach-config"
version = "0.1.26" version = "0.1.27"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"] authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018" edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud" description = "Command line tool for installing, updating and configuring PeachCloud"

View File

@ -42,6 +42,8 @@ pub enum PeachConfigError {
Golgi { source: GolgiError }, Golgi { source: GolgiError },
#[snafu(display("{}", message))] #[snafu(display("{}", message))]
CmdInputError { message: String }, CmdInputError { message: String },
#[snafu(display("{}", message))]
WaitForSbotError { message: String },
} }
impl From<std::io::Error> for PeachConfigError { impl From<std::io::Error> for PeachConfigError {

View File

@ -10,6 +10,7 @@ mod setup_peach_deb;
mod status; mod status;
mod update; mod update;
mod utils; mod utils;
mod wait_for_sbot;
use clap::arg_enum; use clap::arg_enum;
use log::error; use log::error;
@ -61,6 +62,10 @@ enum PeachConfig {
/// It takes an address argument of the form host:port /// It takes an address argument of the form host:port
#[structopt(name = "publish-address")] #[structopt(name = "publish-address")]
PublishAddress(PublishAddressOpts), PublishAddress(PublishAddressOpts),
/// Wait for a successful connection to sbot
#[structopt(name = "wait-for-sbot")]
WaitForSbot,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
@ -193,6 +198,14 @@ async fn run() {
} }
} }
} }
PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await {
Ok(sbot_id) => {
println!("connected with sbot and found sbot_id: {:?}", sbot_id)
}
Err(err) => {
error!("peach-config did not successfully connect to sbot: {}", err)
}
},
} }
} }
} }

View File

@ -0,0 +1,52 @@
use std::{thread, time};
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
static MAX_NUM_ATTEMPTS: u8 = 10;
/// Utility function to wait for a successful whoami call with sbot
/// After each attempt to call whoami it waits 2 seconds,
/// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call
/// it returns an Error. Otherwise it returns Ok(sbot_id).
pub async fn wait_for_sbot() -> Result<String, PeachConfigError> {
let mut num_attempts = 0;
let mut whoami = None;
while num_attempts < MAX_NUM_ATTEMPTS {
let mut sbot = None;
let sbot_res = init_sbot().await;
match sbot_res {
Ok(sbot_instance) => {
sbot = Some(sbot_instance);
}
Err(err) => {
eprintln!("failed to connect to sbot: {:?}", err);
}
}
if sbot.is_some() {
let sbot_id_res = sbot.unwrap().whoami().await;
match sbot_id_res {
Ok(sbot_id) => {
whoami = Some(sbot_id);
break;
}
Err(err) => {
eprintln!("whoami failed: {:?}", err);
}
}
}
println!("trying to connect to sbot again {:?}", num_attempts);
num_attempts += 1;
let sleep_duration = time::Duration::from_secs(2);
thread::sleep(sleep_duration);
}
whoami.ok_or(PeachConfigError::WaitForSbotError {
message: "Failed to find sbot_id after 10 attempts".to_string(),
})
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-lib" name = "peach-lib"
version = "1.3.5" version = "1.3.4"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
@ -9,10 +9,12 @@ async-std = "1.10"
chrono = "0.4" chrono = "0.4"
dirs = "4.0" dirs = "4.0"
fslock="0.1" fslock="0.1"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" } kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" }
tilde-client = { path = "../tilde-client" }
jsonrpc-client-core = "0.5" jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5" jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0" jsonrpc-core = "8.0"
jsonrpc_client = "0.7"
log = "0.4" log = "0.4"
nanorand = { version = "0.6", features = ["getrandom"] } nanorand = { version = "0.6", features = ["getrandom"] }
regex = "1" regex = "1"
@ -22,3 +24,4 @@ serde_yaml = "0.8"
toml = "0.5" toml = "0.5"
sha3 = "0.10" sha3 = "0.10"
lazy_static = "1.4" lazy_static = "1.4"
anyhow = "1.0.86"

View File

@ -57,11 +57,10 @@ pub fn get_peach_config_defaults() -> HashMap<String, String> {
("DYN_NAMESERVER", "ns.peachcloud.org"), ("DYN_NAMESERVER", "ns.peachcloud.org"),
("DYN_ENABLED", "false"), ("DYN_ENABLED", "false"),
("SSB_ADMIN_IDS", ""), ("SSB_ADMIN_IDS", ""),
("SYSTEM_MANAGER", "systemd"),
("ADMIN_PASSWORD_HASH", "47"), ("ADMIN_PASSWORD_HASH", "47"),
("TEMPORARY_PASSWORD_HASH", ""), ("TEMPORARY_PASSWORD_HASH", ""),
("GO_SBOT_DATADIR", "/home/peach/.ssb-go"), ("TILDE_SBOT_DATADIR", "/home/notplants/.local/share/tildefriends/"),
("GO_SBOT_SERVICE", "go-sbot"), ("TILDE_SBOT_SERVICE", "tilde-sbot.service"),
("PEACH_CONFIGDIR", "/var/lib/peachcloud"), ("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
("PEACH_HOMEDIR", "/home/peach"), ("PEACH_HOMEDIR", "/home/peach"),
("PEACH_WEBDIR", "/usr/share/peach-web"), ("PEACH_WEBDIR", "/usr/share/peach-web"),

View File

@ -1,9 +1,9 @@
#![warn(missing_docs)] #![warn(missing_docs)]
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities. //! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
use golgi::GolgiError;
use std::{io, str, string}; use std::{io, str, string};
use jsonrpc_client::JsonRpcError;
use anyhow::Error; // Add the anyhow crate for errors
/// This type represents all possible errors that can occur when interacting with the PeachCloud library. /// This type represents all possible errors that can occur when interacting with the PeachCloud library.
#[derive(Debug)] #[derive(Debug)]
@ -104,11 +104,14 @@ pub enum PeachError {
path: String, path: String,
}, },
/// Represents a Golgi error /// Represents a JsonRpcError with Solar
Golgi(GolgiError), JsonRpcError(JsonRpcError),
/// Represents a generic system error, whose details are specified in the string message /// Represents an Anyhow error with Solar
System(String), SolarClientError(String),
/// Represents an error with encoding or decoding an SsbMessage
SsbMessageError(String),
} }
@ -138,8 +141,9 @@ impl std::error::Error for PeachError {
PeachError::Utf8ToStr(_) => None, PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None, PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source), PeachError::Write { ref source, .. } => Some(source),
PeachError::Golgi(_) => None, PeachError::JsonRpcError(_) => None,
PeachError::System(_) => None, PeachError::SolarClientError(_) => None,
PeachError::SsbMessageError(_) => None,
} }
} }
} }
@ -197,10 +201,9 @@ impl std::fmt::Display for PeachError {
PeachError::Write { ref path, .. } => { PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path) write!(f, "Write error: {}", path)
} }
PeachError::Golgi(ref err) => err.fmt(f), PeachError::JsonRpcError(ref err) => err.fmt(f),
PeachError::System(ref msg) => { PeachError::SolarClientError(ref err) => err.fmt(f),
write!(f, "system error: {}", msg) PeachError::SsbMessageError(ref err) => err.fmt(f),
}
} }
} }
} }
@ -271,9 +274,15 @@ impl From<string::FromUtf8Error> for PeachError {
} }
} }
impl From<GolgiError> for PeachError { impl From<JsonRpcError> for PeachError {
fn from(err: GolgiError) -> PeachError { fn from(err: JsonRpcError) -> PeachError {
PeachError::Golgi(err) PeachError::JsonRpcError(err)
} }
} }
impl From<anyhow::Error> for PeachError {
fn from(error: anyhow::Error) -> Self {
// TODO: include whole error somehow?
PeachError::SolarClientError(error.to_string())
}
}

View File

@ -6,9 +6,11 @@ pub mod oled_client;
pub mod password_utils; pub mod password_utils;
pub mod sbot; pub mod sbot;
pub mod stats_client; pub mod stats_client;
pub mod ssb_messages;
// re-export error types // re-export error types
pub use jsonrpc_client_core; pub use jsonrpc_client_core;
pub use jsonrpc_core; pub use jsonrpc_core;
pub use serde_json; pub use serde_json;
pub use serde_yaml; pub use serde_yaml;
pub use tilde_client;

View File

@ -1,9 +1,9 @@
use async_std::task; use async_std::task;
use golgi::{sbot::Keystore, Sbot};
use log::debug; use log::debug;
use nanorand::{Rng, WyRand}; use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256}; use sha3::{Digest, Sha3_256};
use crate::sbot::init_sbot;
use crate::{config_manager, error::PeachError, sbot::SbotConfig}; use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct, /// Returns Ok(()) if the supplied password is correct,
@ -122,22 +122,15 @@ async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
let recipient = vec![recipient.to_string()]; let recipient = vec![recipient.to_string()];
// initialise sbot connection with ip:port and shscap from config file // initialise sbot connection with ip:port and shscap from config file
let mut sbot_client = match sbot_config { let mut sbot_client = init_sbot();
// TODO: panics if we pass `Some(conf.shscap)` as second arg
Some(conf) => {
let ip_port = conf.lis.clone();
Sbot::init(Keystore::GoSbot, Some(ip_port), None)
.await
.map_err(|e| e.to_string())?
}
None => Sbot::init(Keystore::GoSbot, None, None)
.await
.map_err(|e| e.to_string())?,
};
debug!("Publishing a Scuttlebutt private message with temporary password"); debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.publish_private(msg, recipient).await { // TODO: implement publish private message in solar, and then implement this
Ok(_) => Ok(()), Err(format!("Failed to publish private message: \
Err(e) => Err(format!("Failed to publish private message: {}", e)), private publishing is not yet implemented in solar_client: \
} the message meant to be sent was: {}", msg))
// match sbot_client.publish_private(msg, recipient).await {
// Ok(_) => Ok(()),
// Err(e) => Err(format!("Failed to publish private message: {}", e)),
// }
} }

View File

@ -1,9 +1,9 @@
//! Data types and associated methods for monitoring and configuring go-sbot. //! Data types and associated methods for monitoring and configuring solar-sbot.
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str}; use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
use std::os::linux::raw::ino_t;
use golgi::{sbot::Keystore, Sbot}; use tilde_client::{TildeClient, get_sbot_client};
use log::{debug}; use log::debug;
use crate::config_manager; use crate::config_manager;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -30,7 +30,7 @@ fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
/* SBOT-RELATED TYPES AND METHODS */ /* SBOT-RELATED TYPES AND METHODS */
/// go-sbot process status. /// solar-sbot process status.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SbotStatus { pub struct SbotStatus {
/// Current process state. /// Current process state.
@ -62,50 +62,16 @@ impl Default for SbotStatus {
} }
impl SbotStatus { impl SbotStatus {
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. /// Retrieve statistics for the solar-sbot systemd process by querying `systemctl`.
pub fn read() -> Result<Self, PeachError> { pub fn read() -> Result<Self, PeachError> {
let system_manager = config_manager::get_config_value("SYSTEM_MANAGER")?;
match system_manager.as_str() {
"systemd" => {
SbotStatus::read_from_systemctl()
},
"supervisord" => {
SbotStatus::read_from_supervisorctl()
},
_ => Err(PeachError::System(format!(
"Invalid configuration for SYSTEM_MANAGER: {:?}",
system_manager)))
}
}
pub fn read_from_supervisorctl() -> Result<Self, PeachError> {
let mut status = SbotStatus::default();
let info_output = Command::new("supervisorctl")
.arg("status")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.output()?;
let service_info = std::str::from_utf8(&info_output.stdout)?;
for line in service_info.lines() {
// example line
// go-sbot RUNNING pid 11, uptime 0:04:23
if line.contains("RUNNING") {
// TODO: this should be an enum
status.state = Some("active".to_string());
}
}
Ok(status)
}
pub fn read_from_systemctl() -> Result<Self, PeachError> {
let mut status = SbotStatus::default(); let mut status = SbotStatus::default();
// note this command does not need to be run as sudo // note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl show // because non-privileged users are able to run systemctl show
let service_name = config_manager::get_config_value("TILDE_SBOT_SERVICE")?;
let info_output = Command::new("systemctl") let info_output = Command::new("systemctl")
.arg("show") .arg("show")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?) .arg(service_name)
.arg("--no-page") .arg("--no-page")
.output()?; .output()?;
@ -127,7 +93,7 @@ impl SbotStatus {
// because non-privileged users are able to run systemctl status // because non-privileged users are able to run systemctl status
let status_output = Command::new("systemctl") let status_output = Command::new("systemctl")
.arg("status") .arg("status")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?) .arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?; .output()?;
let service_status = str::from_utf8(&status_output.stdout)?; let service_status = str::from_utf8(&status_output.stdout)?;
@ -135,7 +101,7 @@ impl SbotStatus {
for line in service_status.lines() { for line in service_status.lines() {
// example of the output line we're looking for: // example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor // `Loaded: loaded (/home/glyph/.config/systemd/user/solar-sbot.service; enabled; vendor
// preset: enabled)` // preset: enabled)`
if line.contains("Loaded:") { if line.contains("Loaded:") {
let before_boot_state = line.find(';'); let before_boot_state = line.find(';');
@ -165,10 +131,15 @@ impl SbotStatus {
} }
} }
// TOOD restore this
// get path to blobstore // get path to blobstore
// let blobstore_path = format!(
// "{}/blobs/sha256",
// config_manager::get_config_value("TILDE_SBOT_DATADIR")?
// );
let blobstore_path = format!( let blobstore_path = format!(
"{}/blobs/sha256", "{}",
config_manager::get_config_value("GO_SBOT_DATADIR")? config_manager::get_config_value("TILDE_SBOT_DATADIR")?
); );
// determine the size of the blobstore directory in bytes // determine the size of the blobstore directory in bytes
@ -178,10 +149,10 @@ impl SbotStatus {
} }
} }
/// go-sbot configuration parameters. /// solar-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Default)]
#[serde(default)] #[serde(default)]
pub struct SbotConfig { pub struct Config {
// TODO: maybe define as a Path type? // TODO: maybe define as a Path type?
/// Directory path for the log and indexes. /// Directory path for the log and indexes.
pub repo: String, pub repo: String,
@ -215,7 +186,27 @@ pub struct SbotConfig {
pub repair: bool, pub repair: bool,
} }
/// Default configuration values for go-sbot. // TODO: make this real
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SbotConfig {
pub repo: String,
pub debugdir: String,
pub shscap: String,
pub hmac: String,
pub hops: i8,
pub lis: String,
pub wslis: String,
pub debuglis: String,
pub localadv: bool,
pub localdiscov: bool,
pub enable_ebt: bool,
pub promisc: bool,
pub nounixsock: bool,
pub repair: bool,
}
/// Default configuration values for solar-sbot.
impl Default for SbotConfig { impl Default for SbotConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -238,12 +229,12 @@ impl Default for SbotConfig {
} }
impl SbotConfig { impl SbotConfig {
/// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`. /// Read the solar-sbot `config.toml` file from file and deserialize into `SbotConfig`.
pub fn read() -> Result<Self, PeachError> { pub fn read() -> Result<Self, PeachError> {
// determine path of user's go-sbot config.toml // determine path of user's solar-sbot config.toml
let config_path = format!( let config_path = format!(
"{}/config.toml", "{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")? config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
); );
let config_contents = fs::read_to_string(config_path)?; let config_contents = fs::read_to_string(config_path)?;
@ -253,17 +244,17 @@ impl SbotConfig {
Ok(config) Ok(config)
} }
/// Write the given `SbotConfig` to the go-sbot `config.toml` file. /// Write the given `SbotConfig` to the solar-sbot `config.toml` file.
pub fn write(config: SbotConfig) -> Result<(), PeachError> { pub fn write(config: SbotConfig) -> Result<(), PeachError> {
let repo_comment = "# For details about go-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string(); let repo_comment = "# For details about solar-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string();
// convert the provided `SbotConfig` instance to a string // convert the provided `SbotConfig` instance to a string
let config_string = toml::to_string(&config)?; let config_string = toml::to_string(&config)?;
// determine path of user's go-sbot config.toml // determine path of user's solar-sbot config.toml
let config_path = format!( let config_path = format!(
"{}/config.toml", "{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")? config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
); );
// open config file for writing // open config file for writing
@ -280,7 +271,7 @@ impl SbotConfig {
} }
/// Initialise an sbot client /// Initialise an sbot client
pub async fn init_sbot() -> Result<Sbot, PeachError> { pub async fn init_sbot() -> Result<TildeClient, PeachError> {
// read sbot config from config.toml // read sbot config from config.toml
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
@ -288,15 +279,10 @@ pub async fn init_sbot() -> Result<Sbot, PeachError> {
// initialise sbot connection with ip:port and shscap from config file // initialise sbot connection with ip:port and shscap from config file
let key_path = format!( let key_path = format!(
"{}/secret", "{}/secret",
config_manager::get_config_value("GO_SBOT_DATADIR")? config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
); );
let sbot_client = match sbot_config { // TODO: read this from config
// TODO: panics if we pass `Some(conf.shscap)` as second arg const SERVER_ADDR: &str = "http://127.0.0.1:3030";
Some(conf) => { let sbot_client = get_sbot_client();
let ip_port = conf.lis.clone();
Sbot::init(Keystore::CustomGoSbot(key_path), Some(ip_port), None).await?
}
None => Sbot::init(Keystore::CustomGoSbot(key_path), None, None).await?,
};
Ok(sbot_client) Ok(sbot_client)
} }

View File

@ -0,0 +1,104 @@
//! Message types and conversion methods.
use kuska_ssb::api::dto::content::TypedMessage;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use crate::error::PeachError;
use crate::error::PeachError::SsbMessageError;
/// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library.
/// It is aliased in golgi to fit the naming convention of the other message
/// types: `SsbMessageKVT` and `SsbMessageValue`.
///
/// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`.
pub type SsbMessageContent = TypedMessage;
/// The `value` of an SSB message (the `V` in `KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageValue {
pub previous: Option<String>,
pub author: String,
pub sequence: u64,
pub timestamp: f64,
pub hash: String,
pub content: Value,
pub signature: String,
}
/// Message content types.
#[derive(Debug, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum SsbMessageContentType {
About,
Vote,
Post,
Contact,
Unrecognized,
}
impl SsbMessageValue {
/// Get the type field of the message content as an enum, if found.
///
/// If no `type` field is found or the `type` field is not a string,
/// it returns an `Err(GolgiError::ContentType)`.
///
/// If a `type` field is found but with an unknown string,
/// it returns an `Ok(SsbMessageContentType::Unrecognized)`.
pub fn get_message_type(&self) -> Result<SsbMessageContentType, PeachError> {
let msg_type = self
.content
.get("type")
.ok_or_else(|| SsbMessageError("type field not found".to_string()))?;
let mtype_str: &str = msg_type.as_str().ok_or_else(|| {
SsbMessageError("type field value is not a string as expected".to_string())
})?;
let enum_type = match mtype_str {
"about" => SsbMessageContentType::About,
"post" => SsbMessageContentType::Post,
"vote" => SsbMessageContentType::Vote,
"contact" => SsbMessageContentType::Contact,
_ => SsbMessageContentType::Unrecognized,
};
Ok(enum_type)
}
/// Helper function which returns `true` if this message is of the given type,
/// and `false` if the type does not match or is not found.
pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool {
let self_message_type = self.get_message_type();
match self_message_type {
Ok(mtype) => mtype == message_type,
Err(_err) => false,
}
}
/// Convert the content JSON value into an `SsbMessageContent` `enum`,
/// using the `type` field as a tag to select which variant of the `enum`
/// to deserialize into.
///
/// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details.
pub fn into_ssb_message_content(self) -> Result<SsbMessageContent, PeachError> {
let m: SsbMessageContent = serde_json::from_value(self.content)?;
Ok(m)
}
}
/// An SSB message represented as a key-value-timestamp (`KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageKVT {
pub key: String,
pub value: SsbMessageValue,
pub timestamp: Option<f64>,
pub rts: Option<f64>,
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-network" name = "peach-network"
version = "0.4.2" version = "0.5.0"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021" edition = "2021"
description = "Query and configure network interfaces." description = "Query and configure network interfaces."

View File

@ -14,6 +14,7 @@
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`. //! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
use std::{ use std::{
collections::HashMap,
fs::OpenOptions, fs::OpenOptions,
io::prelude::*, io::prelude::*,
process::{Command, Stdio}, process::{Command, Stdio},
@ -106,8 +107,86 @@ pub struct Traffic {
pub transmitted: u64, pub transmitted: u64,
} }
/// Access point data including state and signal strength.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct AccessPoint {
/// Access point data retrieved via scan.
pub detail: Option<Scan>,
/// Current state of the access point (e.g. "Available" or "Out of range").
pub state: String,
/// Signal strength of the access point as a percentage.
pub signal: Option<i32>,
}
impl AccessPoint {
fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint {
AccessPoint {
detail,
state: String::from("Available"),
signal,
}
}
fn saved() -> AccessPoint {
AccessPoint {
detail: None,
state: String::from("Out of range"),
signal: None,
}
}
}
/* GET - Methods for retrieving data */ /* GET - Methods for retrieving data */
/// Retrieve combined list of available (in-range) and saved wireless access
/// points for a given network interface.
///
/// # Arguments
///
/// * `iface` - A string slice holding the name of a wireless network interface
///
/// If the list results include one or more access points for the given network
/// interface, an `Ok` `Result` type is returned containing `HashMap<String,
/// AccessPoint>`.
///
/// Each entry in the returned `HashMap` contains an SSID (`String`) and
/// `AccessPoint` `struct`. If no access points are found, an empty `HashMap`
/// is returned in the `Result`. In the event of an error, a `NetworkError`
/// is returned in the `Result`.
pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> {
let mut wlan_networks = HashMap::new();
if let Ok(Some(networks)) = available_networks(iface) {
for ap in networks {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint::available(Some(ap), Some(quality_percent));
wlan_networks.insert(ssid, ap_detail);
}
}
if let Ok(Some(networks)) = saved_networks() {
for saved_ssid in networks {
if !wlan_networks.contains_key(&saved_ssid) {
let ssid = saved_ssid.clone();
let ap_detail = AccessPoint::saved();
wlan_networks.insert(ssid, ap_detail);
}
}
}
Ok(wlan_networks)
}
/// Retrieve list of available wireless access points for a given network /// Retrieve list of available wireless access points for a given network
/// interface. /// interface.
/// ///

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-web" name = "peach-web"
version = "0.6.19" version = "0.6.21"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"] authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018" edition = "2018"
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins." description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
@ -33,20 +33,21 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
async-std = "1.10" async-std = { version = "1", features=["attributes", "tokio1"] }
base64 = "0.13" base64 = "0.13"
chrono = "0.4" chrono = "0.4"
dirs = "4.0" dirs = "4.0"
env_logger = "0.8" env_logger = "0.8"
futures = "0.3" futures = "0.3"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
lazy_static = "1.4" lazy_static = "1.4"
log = "0.4" log = "0.4"
maud = "0.23" maud = "0.23"
peach-lib = { path = "../peach-lib" } peach-lib = { path = "../peach-lib" }
# these will be reintroduced when the full peachcloud mode is added peach-network = { path = "../peach-network" }
#peach-network = { path = "../peach-network" } peach-stats = { path = "../peach-stats" }
#peach-stats = { path = "../peach-stats" }
rouille = { version = "3.5", default-features = false } rouille = { version = "3.5", default-features = false }
temporary = "0.6" temporary = "0.6"
vnstat_parse = "0.1.0"
xdg = "2.2" xdg = "2.2"
jsonrpc_client = { version = "0.7", features = ["macros", "reqwest"] }
reqwest = "0.11.24"

View File

@ -2,7 +2,6 @@
use std::io::Error as IoError; use std::io::Error as IoError;
use golgi::GolgiError;
use peach_lib::error::PeachError; use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml}; use peach_lib::{serde_json, serde_yaml};
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
@ -12,28 +11,26 @@ use serde_yaml::Error as YamlError;
#[derive(Debug)] #[derive(Debug)]
pub enum PeachWebError { pub enum PeachWebError {
FailedToRegisterDynDomain(String), FailedToRegisterDynDomain(String),
Golgi(GolgiError),
HomeDir, HomeDir,
Io(IoError), Io(IoError),
Json(JsonError), Json(JsonError),
OsString, OsString,
PeachLib { source: PeachError, msg: String }, PeachLib { source: PeachError, msg: String },
System(String),
Yaml(YamlError), Yaml(YamlError),
NotYetImplemented,
} }
impl std::error::Error for PeachWebError { impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self { match *self {
PeachWebError::FailedToRegisterDynDomain(_) => None, PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::Golgi(ref source) => Some(source),
PeachWebError::HomeDir => None, PeachWebError::HomeDir => None,
PeachWebError::Io(ref source) => Some(source), PeachWebError::Io(ref source) => Some(source),
PeachWebError::Json(ref source) => Some(source), PeachWebError::Json(ref source) => Some(source),
PeachWebError::OsString => None, PeachWebError::OsString => None,
PeachWebError::PeachLib { ref source, .. } => Some(source), PeachWebError::PeachLib { ref source, .. } => Some(source),
PeachWebError::System(_) => None,
PeachWebError::Yaml(ref source) => Some(source), PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::NotYetImplemented => None
} }
} }
} }
@ -44,7 +41,6 @@ impl std::fmt::Display for PeachWebError {
PeachWebError::FailedToRegisterDynDomain(ref msg) => { PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg) write!(f, "DYN DNS error: {}", msg)
} }
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
PeachWebError::HomeDir => write!( PeachWebError::HomeDir => write!(
f, f,
"Filesystem error: failed to determine home directory path" "Filesystem error: failed to determine home directory path"
@ -56,20 +52,12 @@ impl std::fmt::Display for PeachWebError {
"Filesystem error: failed to convert OsString to String for go-ssb directory path" "Filesystem error: failed to convert OsString to String for go-ssb directory path"
), ),
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source), PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
PeachWebError::System(ref msg) => {
write!(f, "system error: {}", msg)
}
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source), PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::NotYetImplemented => write!(f, "Not yet implemented"),
} }
} }
} }
impl From<GolgiError> for PeachWebError {
fn from(err: GolgiError) -> PeachWebError {
PeachWebError::Golgi(err)
}
}
impl From<IoError> for PeachWebError { impl From<IoError> for PeachWebError {
fn from(err: IoError) -> PeachWebError { fn from(err: IoError) -> PeachWebError {
PeachWebError::Io(err) PeachWebError::Io(err)

View File

@ -39,6 +39,12 @@ lazy_static! {
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light); static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
} }
/// Wireless interface identifier.
pub const WLAN_IFACE: &str = "wlan0";
/// Access point interface identifier.
pub const AP_IFACE: &str = "ap0";
/// Session data for each authenticated client. /// Session data for each authenticated client.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SessionData { pub struct SessionData {

View File

@ -166,6 +166,18 @@ pub fn mount_peachpub_routes(
routes::settings::admin::delete::handle_form(request) routes::settings::admin::delete::handle_form(request)
}, },
(GET) (/settings/power) => {
Response::html(routes::settings::power::menu::build_template(request))
},
(GET) (/settings/power/reboot) => {
routes::settings::power::reboot::handle_reboot()
},
(GET) (/settings/power/shutdown) => {
routes::settings::power::shutdown::handle_shutdown()
},
(GET) (/settings/scuttlebutt) => { (GET) (/settings/scuttlebutt) => {
Response::html(routes::settings::scuttlebutt::menu::build_template(request)) Response::html(routes::settings::scuttlebutt::menu::build_template(request))
.reset_flash() .reset_flash()
@ -200,14 +212,66 @@ pub fn mount_peachpub_routes(
routes::settings::scuttlebutt::default::write_config() routes::settings::scuttlebutt::default::write_config()
}, },
(GET) (/settings/network) => {
Response::html(routes::settings::network::menu::build_template(request)).reset_flash()
},
(GET) (/settings/network/dns) => {
Response::html(routes::settings::network::configure_dns::build_template(request)).reset_flash()
},
(POST) (/settings/network/dns) => {
routes::settings::network::configure_dns::handle_form(request)
},
(GET) (/settings/network/wifi) => {
Response::html(routes::settings::network::list_aps::build_template())
},
(GET) (/settings/network/wifi/add) => {
Response::html(routes::settings::network::add_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/add) => {
routes::settings::network::add_ap::handle_form(request)
},
(GET) (/settings/network/wifi/add/{ssid: String}) => {
Response::html(routes::settings::network::add_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/modify) => {
Response::html(routes::settings::network::modify_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/modify) => {
routes::settings::network::modify_ap::handle_form(request)
},
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
Response::html(routes::settings::network::modify_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/{ssid: String}) => {
Response::html(routes::settings::network::ap_details::build_template(request, ssid))
},
(GET) (/settings/theme/{theme: String}) => { (GET) (/settings/theme/{theme: String}) => {
routes::settings::theme::set_theme(theme) routes::settings::theme::set_theme(theme)
}, },
(GET) (/status) => {
Response::html(routes::status::device::build_template())
},
(GET) (/status/scuttlebutt) => { (GET) (/status/scuttlebutt) => {
Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt") Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt")
}, },
(GET) (/status/network) => {
Response::html(routes::status::network::build_template())
},
// render the not_found template and set a 404 status code if none of // render the not_found template and set a 404 status code if none of
// the other blocks matches the request // the other blocks matches the request
_ => Response::html(templates::not_found::build_template()).with_status_code(404) _ => Response::html(templates::not_found::build_template()).with_status_code(404)

View File

@ -1,7 +1,7 @@
use maud::{html, PreEscaped}; use maud::{html, PreEscaped};
use peach_lib::sbot::SbotStatus; use peach_lib::sbot::SbotStatus;
use crate::{templates, utils::theme}; use crate::{templates, utils::theme, SERVER_CONFIG};
/// Read the state of the go-sbot process and define status-related /// Read the state of the go-sbot process and define status-related
/// elements accordingly. /// elements accordingly.
@ -24,9 +24,23 @@ fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
} }
} }
/// Render the URL for the status element (icon / link).
///
/// If the application is running in standalone mode then the element links
/// directly to the Scuttlebutt status page. If not, it links to the device
/// status page.
fn render_status_url<'a>() -> &'a str {
if SERVER_CONFIG.standalone_mode {
"/status/scuttlebutt"
} else {
"/status"
}
}
/// Home template builder. /// Home template builder.
pub fn build_template() -> PreEscaped<String> { pub fn build_template() -> PreEscaped<String> {
let (circle_color, center_circle_text, circle_border) = render_status_elements(); let (circle_color, center_circle_text, circle_border) = render_status_elements();
let status_url = render_status_url();
// render the home template html // render the home template html
let home_template = html! { let home_template = html! {
@ -63,7 +77,7 @@ pub fn build_template() -> PreEscaped<String> {
} }
(PreEscaped("<!-- bottom-left -->")) (PreEscaped("<!-- bottom-left -->"))
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->")) (PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
a class="bottom-left" href="/status/scuttlebutt" title="Status" { a class="bottom-left" href=(status_url) title="Status" {
div class={ "circle circle-small border-circle-small " (circle_border) } { div class={ "circle circle-small border-circle-small " (circle_border) } {
img class="icon-medium" src="/icons/heart-pulse.svg"; img class="icon-medium" src="/icons/heart-pulse.svg";
} }

View File

@ -11,8 +11,10 @@ pub fn build_template() -> PreEscaped<String> {
div class="card center" { div class="card center" {
(PreEscaped("<!-- BUTTONS -->")) (PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" { div id="settingsButtons" {
// render the network settings button if we're not in standalone mode // render the network settings and power menu buttons if we're
// not in standalone mode
@if !SERVER_CONFIG.standalone_mode { @if !SERVER_CONFIG.standalone_mode {
a id="power" class="button button-primary center" href="/settings/power" title="Power Menu" { "Power" }
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" } a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
} }
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" } a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }

View File

@ -1,6 +1,7 @@
pub mod admin; pub mod admin;
//pub mod dns; //pub mod dns;
pub mod menu; pub mod menu;
//pub mod network; pub mod network;
pub mod power;
pub mod scuttlebutt; pub mod scuttlebutt;
pub mod theme; pub mod theme;

View File

@ -1,322 +0,0 @@
use log::{debug, warn};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
uri, UriDisplayQuery,
};
use rocket_dyn_templates::{tera::Context, Template};
use peach_network::network;
use crate::{
context,
context::network::{NetworkAlertContext, NetworkDetailContext, NetworkListContext},
routes::authentication::Authenticated,
utils::{monitor, monitor::Threshold},
AP_IFACE, WLAN_IFACE,
};
// STRUCTS USED BY NETWORK ROUTES
#[derive(Debug, FromForm, UriDisplayQuery)]
pub struct Ssid {
pub ssid: String,
}
#[derive(Debug, FromForm)]
pub struct WiFi {
pub ssid: String,
pub pass: String,
}
// HELPERS AND ROUTES FOR /settings/network/wifi/usage/reset
#[get("/wifi/usage/reset")]
pub fn wifi_usage_reset(_auth: Authenticated) -> Flash<Redirect> {
let url = uri!(wifi_usage);
match monitor::reset_data() {
Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to reset stored network traffic total",
),
}
}
#[post("/wifi/connect", data = "<network>")]
pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_detail(ssid = ssid));
match network::id(&*WLAN_IFACE, ssid) {
Ok(Some(id)) => match network::connect(&id, &*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
},
_ => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
}
}
#[post("/wifi/disconnect", data = "<network>")]
pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network::disable(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"),
}
}
#[post("/wifi/forget", data = "<network>")]
pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network::forget(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to remove WiFi credentials".to_string(),
),
}
}
#[get("/wifi/modify?<ssid>")]
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network/wifi".to_string()));
context.insert("title", &Some("Update WiFi Password".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/modify_ap", &context.into_json())
}
#[post("/wifi/modify", data = "<wifi>")]
pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &wifi.ssid;
let pass = &wifi.pass;
let url = uri!(network_detail(ssid = ssid));
match network::update(&*WLAN_IFACE, ssid, pass) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to update WiFi password".to_string(),
),
}
}
// HELPERS AND ROUTES FOR /settings/network
#[get("/")]
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context
let mut context = Context::new();
context.insert("back", &Some("/settings"));
context.insert("title", &Some("Network Configuration"));
context.insert("ap_state", &context::network::ap_state());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/menu", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/network/ap/activate
#[get("/ap/activate")]
pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless access point
debug!("Activating WiFi access point.");
match network::start_iface_service(&*AP_IFACE) {
Ok(_) => Flash::success(
Redirect::to("/settings/network"),
"Activated WiFi access point",
),
Err(_) => Flash::error(
Redirect::to("/settings/network"),
"Failed to activate WiFi access point",
),
}
}
// HELPERS AND ROUTES FOR /settings/network/wifi
#[get("/wifi")]
pub fn wifi_list(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkListContext::build();
context.back = Some("/settings/network".to_string());
context.title = Some("WiFi Networks".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/list_aps", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
#[get("/wifi?<ssid>")]
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkDetailContext::build();
context.back = Some("/settings/network/wifi".to_string());
context.title = Some("WiFi Network".to_string());
context.selected = Some(ssid.to_string());
if let Some(flash) = flash {
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/ap_details", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi/activate
#[get("/wifi/activate")]
pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless client
debug!("Activating WiFi client mode.");
match network::start_iface_service(&*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to("/settings/network"), "Activated WiFi client"),
Err(_) => Flash::error(
Redirect::to("/settings/network"),
"Failed to activate WiFi client",
),
}
}
// HELPERS AND ROUTES FOR /settings/network/wifi/add
#[get("/wifi/add")]
pub fn add_wifi(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/add_ap", &context.into_json())
}
#[get("/wifi/add?<ssid>")]
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/add_ap", &context.into_json())
}
#[post("/wifi/add", data = "<wifi>")]
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check if the credentials already exist for this access point
// note: this is nicer but it's an unstable feature:
// if check_saved_aps(&wifi.ssid).contains(true)
// use unwrap_or instead, set value to false if err is returned
//let creds_exist = network::saved_networks(&wifi.ssid).unwrap_or(false);
let creds_exist = match network::saved_networks() {
Ok(Some(networks)) => networks.contains(&wifi.ssid),
_ => false,
};
// if credentials not found, generate and write wifi config to wpa_supplicant
let (flash_name, flash_msg) = if creds_exist {
(
"error".to_string(),
"Network credentials already exist for this access point".to_string(),
)
} else {
match network::add(&*WLAN_IFACE, &wifi.ssid, &wifi.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
}
("success".to_string(), "Added WiFi credentials".to_string())
}
Err(e) => {
debug!("Failed to add WiFi credentials.");
("error".to_string(), format!("{}", e))
}
}
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/network/add_ap", &context.into_json())
}
// HELPERS AND ROUTES FOR WIFI USAGE
#[get("/wifi/usage")]
pub fn wifi_usage(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAlertContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Network Data Usage".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("settings/network/data_usage_limits", &context)
}
#[post("/wifi/usage", data = "<thresholds>")]
pub fn wifi_usage_alerts(thresholds: Form<Threshold>, _auth: Authenticated) -> Flash<Redirect> {
match monitor::update_store(thresholds.into_inner()) {
Ok(_) => {
debug!("WiFi data usage thresholds updated.");
Flash::success(
Redirect::to("/settings/network/wifi/usage"),
"Updated alert thresholds and flags",
)
}
Err(_) => {
warn!("Failed to update WiFi data usage thresholds.");
Flash::error(
Redirect::to("/settings/network/wifi/usage"),
"Failed to update alert thresholds and flags",
)
}
}
}

View File

@ -0,0 +1,97 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
WLAN_IFACE,
};
// ROUTE: /settings/network/wifi/add
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
html! {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
}
}
fn render_password_input() -> Markup {
html! {
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi access point credentials form template builder.
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- WIFI ADD CREDENTIALS FORM -->"))
div class="card center" {
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
(render_ssid_input(selected_ap))
(render_password_input())
(render_buttons())
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
form_template,
"Add WiFi Network",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
/// Parse the SSID and password for an access point and save the new credentials.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
ssid: String,
pass: String,
}));
let (name, msg) = match network::add(WLAN_IFACE, &data.ssid, &data.pass) {
Ok(_) => match network::reconfigure() {
Ok(_) => ("success".to_string(), "Added WiFi credentials".to_string()),
Err(err) => (
"error".to_string(),
format!(
"Added WiFi credentials but failed to reconfigure interface: {}",
err
),
),
},
Err(err) => (
"error".to_string(),
format!("Failed to add WiFi credentials for {}: {}", &data.ssid, err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/wifi/add").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,197 @@
use std::collections::HashMap;
use maud::{html, Markup, PreEscaped};
use peach_network::{network, network::AccessPoint, NetworkError};
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
WLAN_IFACE,
};
// ROUTE: /settings/network/wifi?<ssid>
fn render_network_status_icon(ssid: &str, wlan_ssid: &str, ap_state: &str) -> Markup {
let status_label_value = if ssid == wlan_ssid {
"CONNECTED"
} else if ap_state == "Available" {
"AVAILABLE"
} else {
"NOT IN RANGE"
};
html! {
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
div class="grid-column-1" {
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (status_label_value) }
}
}
}
fn render_network_detailed_info(ssid: &str, ap_protocol: &str, ap_signal: Option<i32>) -> Markup {
let ap_signal_value = match ap_signal {
Some(signal) => signal.to_string(),
None => "Unknown".to_string(),
};
html! {
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
div class="grid-column-2" {
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" };
p id="netSsid" class="card-text" title="SSID" { (ssid) }
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" };
p id="netSec" class="card-text" title={ "Security protocol in use by " (ssid) } { (ap_protocol) }
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" };
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal_value) }
}
}
}
fn render_disconnect_form(ssid: &str) -> Markup {
html! {
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
}
}
}
fn render_connect_form(ssid: &str) -> Markup {
html! {
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
}
}
}
fn render_forget_form(ssid: &str) -> Markup {
html! {
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
}
}
}
fn render_buttons(
selected_ap: &str,
wlan_ssid: &str,
ap: &AccessPoint,
saved_wifi_networks: Vec<String>,
) -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
@if wlan_ssid == selected_ap {
(render_disconnect_form(selected_ap))
}
@if saved_wifi_networks.contains(&selected_ap.to_string()) {
@if wlan_ssid != selected_ap && ap.state == "Available" {
(render_connect_form(selected_ap))
}
a class="button button-primary center" href={ "/settings/network/wifi/modify?ssid=" (selected_ap) } { "Modify" }
(render_forget_form(selected_ap))
} @else {
// display the Add button if AP creds not already in saved
// networks list
a class="button button-primary center" href={ "/settings/network/wifi/add?ssid=" (selected_ap) } { "Add" }
}
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
}
}
}
/// Retrieve the list of all saved and in-range networks (including SSID and
/// AP details for each network), the list of all saved networks (SSIDs only)
/// and the SSID for the WiFi interface.
fn retrieve_network_data() -> (
Result<HashMap<String, AccessPoint>, NetworkError>,
Vec<String>,
String,
) {
let all_wifi_networks = network::all_networks(WLAN_IFACE);
let saved_wifi_networks = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => String::from("Not connected"),
};
(all_wifi_networks, saved_wifi_networks, wlan_ssid)
}
/// WiFi access point (AP) template builder.
///
/// Render a UI card with details about the selected access point, including
/// the connection state, security protocol being used, the SSID and the
/// signal strength. Buttons are also rendering based on the state of the
/// access point and whether or not credentials for the AP have previously
/// been saved.
///
/// If the AP is available (ie. in-range) then a Connect button is rendered.
/// A Disconnect button is rendered if the WiFi client is currently
/// connected to the AP.
///
/// If credentials have not previously been saved for the AP, an Add button is
/// rendered. Forget and Modify buttons are rendered if credentials for the AP
/// have previously been saved.
pub fn build_template(request: &Request, selected_ap: String) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let (all_wifi_networks, saved_wifi_networks, wlan_ssid) = retrieve_network_data();
let network_info_box_class = if selected_ap == wlan_ssid {
"two-grid capsule success-border"
} else {
"two-grid capsule"
};
let network_list_template = html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
@if let Ok(wlan_networks) = all_wifi_networks {
// select only the access point we are interested in displaying
@if let Some((ssid, ap)) = wlan_networks.get_key_value(&selected_ap) {
@let ap_protocol = match &ap.detail {
Some(detail) => detail.protocol.clone(),
None => "None".to_string()
};
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(network_info_box_class) title="PeachCloud network mode and status" {
(PreEscaped("<!-- left column -->"))
(render_network_status_icon(ssid, &wlan_ssid, &ap.state))
(PreEscaped("<!-- right column -->"))
(render_network_detailed_info(ssid, &ap_protocol, ap.signal))
}
(render_buttons(ssid, &wlan_ssid, ap, saved_wifi_networks))
} @else {
p class="card-text list-item" { (selected_ap) " not found in saved or in-range networks" }
}
} @else {
p class="card-text list-item" { "No saved or in-range networks found" }
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
network_list_template,
"WiFi Networks",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,201 @@
use log::info;
use maud::{html, Markup, PreEscaped};
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
};
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
error::PeachWebError,
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// ROUTE: /settings/network/dns
fn render_dyndns_status_indicator() -> Markup {
let (indicator_class, indicator_label) = match dyndns_client::is_dns_updater_online() {
Ok(true) => ("success-border", "Dynamic DNS is currently online."),
_ => (
"warning-border",
"Dynamic DNS is enabled but may be offline.",
),
};
html! {
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
div id="dyndns-status-indicator" class={ "stack capsule " (indicator_class) } {
div class="stack" {
label class="label-small font-near-black" { (indicator_label) }
}
}
}
}
fn render_external_domain_input() -> Markup {
let external_domain = config_manager::get_config_value("EXTERNAL_DOMAIN").ok();
html! {
div class="input-wrapper" {
(PreEscaped("<!-- input for externaldomain -->"))
label id="external_domain" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=[external_domain];
}
}
}
}
fn render_dyndns_enabled_checkbox() -> Markup {
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
html! {
div class="input-wrapper" {
div {
(PreEscaped("<!-- checkbox for dyndns flag -->"))
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dynamic DNS" type="checkbox" checked[dyndns_enabled];
}
}
}
}
fn render_dynamic_domain_input() -> Markup {
let dyndns_domain =
config_manager::get_config_value("DYN_DOMAIN").unwrap_or_else(|_| String::from(""));
let dyndns_subdomain =
dyndns_client::get_dyndns_subdomain(&dyndns_domain).unwrap_or(dyndns_domain);
html! {
div class="input-wrapper" {
(PreEscaped("<!-- input for dyndns domain -->"))
label id="cut" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(dyndns_subdomain);
{ ".dyn.peachcloud.org" }
}
}
}
}
fn render_save_button() -> Markup {
html! {
div id="buttonDiv" style="margin-top: 2rem;" {
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
}
}
}
/// DNS configuration form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
let form_template = html! {
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
div class="card center" {
@if dyndns_enabled {
(render_dyndns_status_indicator())
}
form id="configureDNS" class="center" action="/settings/network/dns" method="post" {
(render_external_domain_input())
(render_dyndns_enabled_checkbox())
(render_dynamic_domain_input())
(render_save_button())
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
}
};
let body = templates::nav::build_template(
form_template,
"Configure Dynamic DNS",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
pub fn save_dns_configuration(
external_domain: String,
enable_dyndns: bool,
dynamic_domain: String,
) -> Result<(), PeachWebError> {
// first save local configurations
config_manager::set_external_domain(&external_domain)?;
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
// if dynamic dns is enabled and this is a new domain name, then register it
if enable_dyndns && dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)? {
if let Err(registration_err) = dyndns_client::register_domain(&full_dynamic_domain) {
info!("Failed to register dyndns domain: {:?}", registration_err);
// error message describing the failed update
let err_msg = match registration_err {
PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(rpc_err), _)) => {
if let ErrorCode::ServerError(-32030) = rpc_err.code {
format!(
"Error registering domain: {} was previously registered",
full_dynamic_domain
)
} else {
format!("Failed to register dyndns domain: {:?}", rpc_err)
}
}
_ => "Failed to register dyndns domain".to_string(),
};
Err(PeachWebError::FailedToRegisterDynDomain(err_msg))
} else {
info!("Registered new dyndns domain");
Ok(())
}
} else {
info!("Domain {} already registered", dynamic_domain);
Ok(())
}
}
/// Parse the DNS configuration parameters and apply them.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
external_domain: String,
enable_dyndns: bool,
dynamic_domain: String,
}));
let (name, msg) = match save_dns_configuration(
data.external_domain,
data.enable_dyndns,
data.dynamic_domain,
) {
Ok(_) => (
"success".to_string(),
"New dynamic DNS configuration is now enabled".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to save DNS configuration: {}", err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/dns").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,164 @@
// TODO:
//
// This template and associated feature set requires vnstat_parse.
// - https://crates.io/crates/vnstat_parse
//
// Use the PeachCloud config system to store warning and cutoff flags,
// as well as the associated totals (thresholds):
//
// - DATA_WARNING_ENABLED
// - DATA_WARNING_LIMIT
// - DATA_CUTOFF_ENABLED
// - DATA_CUTOFF_LIMIT
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::Request;
use vnstat_parse::Vnstat;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
WLAN_IFACE,
};
// ROUTE: /settings/network/wifi/usage
fn render_data_usage_total_capsule() -> Markup {
html! {
div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;" {
div class="flex-grid" {
label id="dataTotal" class="label-large" title="Data download total in MB" {
data_total.total / 1024 / 1024 | round
}
label class="label-small font-near-black" { "MB" }
}
label class="center-text label-small font-gray" { "USAGE TOTAL" }
}
}
}
fn render_warning_threshold_icon() -> Markup {
// threshold.warn_flag
let warning_enabled = true;
let icon_class = match warning_enabled {
true => "icon",
false => "icon icon-inactive",
};
html! {
div class="card-container container" {
div {
img id="warnIcon" class=(icon_class) alt="Warning" title="Warning threshold" src="/icons/alert.svg";
}
}
}
}
fn render_warning_threshold_input() -> Markup {
// TODO: source threshold.warn value and replace below
html! {
div {
(PreEscaped("<!-- input for warning threshold -->"))
label id="warn" class="label-small font-near-black" {
input id="warnInput" class="alert-input" name="warn" placeholder="0" type="text" title="Warning threshold value" value="{{ threshold.warn }}" { "MB" }
}
label class="label-small font-gray" for="warn" style="padding-top: 0.25rem;" { "WARNING THRESHOLD" }
}
}
}
fn render_warning_threshold_checkbox() -> Markup {
let warning_enabled = true;
html! {
div {
(PreEscaped("<!-- checkbox for warning threshold flag -->"))
input id="warnCheck" name="warn_flag" title="Activate warning" type="checkbox" checked[warning_enabled];
}
}
}
fn render_critical_threshold_icon() -> Markup {
// threshold.cut_flag
let cutoff_enabled = true;
let icon_class = match cutoff_enabled {
true => "icon",
false => "icon icon-inactive",
};
html! {
div {
img id="cutIcon"
class=(icon_class)
alt="Cutoff"
title="Cutoff threshold"
src="/icons/scissor.svg";
}
}
}
fn render_critical_threshold_input() -> Markup {
// TODO: source threshold.cut value and replace below
html! {
div {
(PreEscaped("<!-- input for cutoff threshold -->"))
label id="cut" class="label-small font-near-black"><input id="cutInput" class="alert-input" name="cut" placeholder="0" type="text" title="Critical threshold value" value="{{ threshold.cut }}" { "MB" }
label class="label-small font-gray" for="cut" style="padding-top: 0.25rem;" { "CUTOFF THRESHOLD" }
}
}
}
fn render_critical_threshold_checkbox() -> Markup {
// threshold.cut_flag
let cutoff_enabled = true;
html! {
div {
(PreEscaped("<!-- checkbox for cutoff threshold flag -->"))
input id="cutCheck" name="cut_flag" title="Activate cutoff" type="checkbox" checked[cutoff_enabled];
}
}
}
fn render_buttons() -> Markup {
html! {
div id="buttonDiv" class="button-div" {
input id="updateAlerts" class="button button-primary center" title="Update" type="submit" value="Update";
a id="resetTotal" class="button button-warning center" href="/settings/network/wifi/usage/reset" title="Reset stored usage total to zero" { "Reset" }
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi data usage form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let wlan_data = Vnstat::get(WLAN_IFACE);
// wlan_data.all_time_total
// wlan_data.all_time_total_unit
let form_template = html! {
(PreEscaped("<!-- NETWORK DATA ALERTS FORM -->"))
form id="wifiAlerts" action="/network/wifi/usage" class="card center" method="post" {
(render_data_usage_total_capsule())
(render_warning_threshold_icon())
(render_warning_threshold_input())
(render_warning_threshold_checkbox())
(render_critical_threshold_icon())
(render_critical_threshold_input())
(render_critical_threshold_checkbox())
(render_buttons())
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
};
}

View File

@ -0,0 +1,106 @@
use std::collections::HashMap;
use maud::{html, Markup, PreEscaped};
use peach_network::{network, network::AccessPoint};
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
// ROUTE: /settings/network/wifi
/// Retrieve network state data required by the WiFi network list template.
fn get_network_state_data(ap: &str, wlan: &str) -> (String, String, HashMap<String, AccessPoint>) {
let ap_state = match network::state(ap) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_ssid = match network::ssid(wlan) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let network_list = match network::all_networks(wlan) {
Ok(networks) => networks,
Err(_) => HashMap::new(),
};
(ap_state, wlan_ssid, network_list)
}
fn render_network_connected_elements(ssid: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link primary-bg" href=(ap_detail_url) {
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi online";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
}
}
}
fn render_network_available_elements(ssid: String, ap_state: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link light-bg" href=(ap_detail_url) {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
}
}
}
fn render_network_unavailable_elements(ssid: String, ap_state: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link" href=(ap_detail_url) {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
}
}
}
/// WiFi network list template builder.
pub fn build_template() -> PreEscaped<String> {
let (ap_state, wlan_ssid, network_list) = get_network_state_data(AP_IFACE, WLAN_IFACE);
let list_template = html! {
div class="card center" {
div class="center list-container" {
ul class="list" {
@if ap_state == "up" {
li class="list-item light-bg warning-border" {
"Enable WiFi client mode to view saved and available networks."
}
} @else if network_list.is_empty() {
li class="list-item light-bg" {
"No saved or available networks found."
}
} @else {
@for (ssid, ap) in network_list {
li {
@if ssid == wlan_ssid {
(render_network_connected_elements(ssid))
} @else if ap.state == "Available" {
(render_network_available_elements(ssid, ap.state))
} @else {
(render_network_unavailable_elements(ssid, ap.state))
}
}
}
}
}
}
}
};
let body =
templates::nav::build_template(list_template, "WiFi Networks", Some("/settings/network"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,65 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
AP_IFACE,
};
// ROUTE: /settings/network
/// Read the wireless interface mode (WiFi AP or client) and selectively render
/// the activation button for the deactivated mode.
fn render_mode_toggle_button() -> Markup {
match network::state(AP_IFACE) {
Ok(Some(state)) if state == "up" => {
html! {
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
}
}
_ => html! {
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
},
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
(PreEscaped("<!-- if ap is up, show 'Enable WiFi' button, else show 'Deplay Access Point' -->"))
(render_mode_toggle_button())
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
// TODO: uncomment this once data usage feature is in place
// a id="viewUsage" class="button button-primary center" href="/settings/network/wifi/usage" title="View Data Usage" { "View Data Usage" }
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
}
}
}
/// Network settings menu template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let menu_template = html! {
(PreEscaped("<!-- NETWORK SETTINGS MENU -->"))
div class="card center" {
(render_buttons())
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(menu_template, "Network Settings", Some("/settings"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,8 @@
pub mod add_ap;
pub mod ap_details;
pub mod configure_dns;
// TODO: uncomment this once data usage feature is in place
// pub mod data_usage_limits;
pub mod list_aps;
pub mod menu;
pub mod modify_ap;

View File

@ -0,0 +1,105 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
WLAN_IFACE,
};
// ROUTE: /settings/network/wifi/modify?<ssid>
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
html! {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
}
}
fn render_password_input() -> Markup {
html! {
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi access point password modification form template builder.
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
div class="card center" {
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
(render_ssid_input(selected_ap))
(render_password_input())
(render_buttons())
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
form_template,
"Change WiFi Password",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
/// Parse the SSID and password for an access point and save the new password.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
ssid: String,
pass: String,
}));
let (name, msg) = match network::id(WLAN_IFACE, &data.ssid) {
Ok(Some(id)) => match network::modify(&id, &data.ssid, &data.pass) {
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
Err(err) => (
"error".to_string(),
format!("Failed to update WiFi password: {}", err),
),
},
Ok(None) => (
"error".to_string(),
format!(
"Failed to update WiFi password: no saved credentials found for network {}",
&data.ssid
),
),
Err(err) => (
"error".to_string(),
format!(
"Failed to update WiFi password: no ID found for network {}: {}",
&data.ssid, err
),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/wifi/modify").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,37 @@
use maud::{html, PreEscaped};
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
};
/// Power menu template builder.
///
/// Presents options for rebooting or shutting down the device.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let power_menu_template = html! {
(PreEscaped("<!-- POWER MENU -->"))
div class="card center" {
div class="card-container" {
div id="buttons" {
a id="rebootBtn" class="button button-primary center" href="/reboot" title="Reboot Device" { "Reboot" }
a id="shutdownBtn" class="button button-warning center" href="/shutdown" title="Shutdown Device" { "Shutdown" }
a id="cancelBtn" class="button button-secondary center" href="/settings" title="Cancel" { "Cancel" }
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
}
};
let body = templates::nav::build_template(power_menu_template, "Power Menu", Some("/"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,3 @@
pub mod menu;
pub mod reboot;
pub mod shutdown;

View File

@ -0,0 +1,36 @@
use log::info;
use rouille::Response;
use std::{
io::Result,
process::{Command, Output},
};
use crate::utils::flash::FlashResponse;
/// Executes a system command to reboot the device immediately.
fn reboot() -> Result<Output> {
info!("Rebooting the device");
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
// response but this is not possible with the `shutdown` command alone.
// TODO: send "rebooting..." message to `peach-oled` for display
Command::new("sudo")
.arg("shutdown")
.arg("-r")
.arg("now")
.output()
}
pub fn handle_reboot() -> Response {
let (name, msg) = match reboot() {
Ok(_) => ("success".to_string(), "Rebooting the device".to_string()),
Err(err) => (
"error".to_string(),
format!("Failed to reboot the device: {}", err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/power").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,35 @@
use log::info;
use rouille::Response;
use std::{
io::Result,
process::{Command, Output},
};
use crate::utils::flash::FlashResponse;
/// Executes a system command to shutdown the device immediately.
fn shutdown() -> Result<Output> {
info!("Shutting down the device");
// ideally, we'd like to shutdown after 5 seconds to allow time for JSON
// response but this is not possible with the `shutdown` command alone.
// TODO: send "shutting down..." message to `peach-oled` for display
Command::new("sudo").arg("shutdown").arg("now").output()
}
pub fn handle_shutdown() -> Response {
let (name, msg) = match shutdown() {
Ok(_) => (
"success".to_string(),
"Shutting down the device".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to shutdown the device: {}", err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/power").add_flash(flash_name, flash_msg)
}

View File

@ -205,7 +205,7 @@ pub fn handle_form(request: &Request, restart: bool) -> Response {
debugdir: String, debugdir: String,
shscap: String, shscap: String,
hmac: String, hmac: String,
hops: u8, hops: i8,
lis_ip: String, lis_ip: String,
lis_port: String, lis_port: String,
wslis: String, wslis: String,
@ -243,13 +243,13 @@ pub fn handle_form(request: &Request, restart: bool) -> Response {
match data.startup { match data.startup {
true => { true => {
debug!("Enabling go-sbot.service"); debug!("Enabling go-sbot.service");
if let Err(e) = sbot::system_sbot_cmd("enable") { if let Err(e) = sbot::systemctl_sbot_cmd("enable") {
warn!("Failed to enable go-sbot.service: {}", e) warn!("Failed to enable go-sbot.service: {}", e)
} }
} }
false => { false => {
debug!("Disabling go-sbot.service"); debug!("Disabling go-sbot.service");
if let Err(e) = sbot::system_sbot_cmd("disable") { if let Err(e) = sbot::systemctl_sbot_cmd("disable") {
warn!("Failed to disable go-sbot.service: {}", e) warn!("Failed to disable go-sbot.service: {}", e)
} }
} }

View File

@ -1,7 +1,7 @@
use log::info; use log::info;
use rouille::Response; use rouille::Response;
use crate::utils::{flash::FlashResponse, sbot}; use crate::utils::{flash::FlashResponse, sbot::systemctl_sbot_cmd};
// ROUTE: /settings/scuttlebutt/restart // ROUTE: /settings/scuttlebutt/restart
@ -10,9 +10,9 @@ use crate::utils::{flash::FlashResponse, sbot};
/// the attempt via a flash message. /// the attempt via a flash message.
pub fn restart_sbot() -> Response { pub fn restart_sbot() -> Response {
info!("Restarting go-sbot.service"); info!("Restarting go-sbot.service");
let (flash_name, flash_msg) = match sbot::system_sbot_cmd("stop") { let (flash_name, flash_msg) = match systemctl_sbot_cmd("stop") {
// if stop was successful, try to start the process // if stop was successful, try to start the process
Ok(_) => match sbot::system_sbot_cmd("start") { Ok(_) => match systemctl_sbot_cmd("start") {
Ok(_) => ( Ok(_) => (
"flash_name=success".to_string(), "flash_name=success".to_string(),
"flash_msg=Sbot process has been restarted".to_string(), "flash_msg=Sbot process has been restarted".to_string(),

View File

@ -1,7 +1,7 @@
use log::info; use log::info;
use rouille::Response; use rouille::Response;
use crate::utils::{flash::FlashResponse, sbot}; use crate::utils::{flash::FlashResponse, sbot::systemctl_sbot_cmd};
// ROUTE: /settings/scuttlebutt/start // ROUTE: /settings/scuttlebutt/start
@ -10,7 +10,7 @@ use crate::utils::{flash::FlashResponse, sbot};
/// the attempt via a flash message. /// the attempt via a flash message.
pub fn start_sbot() -> Response { pub fn start_sbot() -> Response {
info!("Starting go-sbot.service"); info!("Starting go-sbot.service");
let (flash_name, flash_msg) = match sbot::system_sbot_cmd("start") { let (flash_name, flash_msg) = match systemctl_sbot_cmd("start") {
Ok(_) => ( Ok(_) => (
"flash_name=success".to_string(), "flash_name=success".to_string(),
"flash_msg=Sbot process has been started".to_string(), "flash_msg=Sbot process has been started".to_string(),

View File

@ -1,7 +1,7 @@
use log::info; use log::info;
use rouille::Response; use rouille::Response;
use crate::utils::{flash::FlashResponse, sbot}; use crate::utils::{flash::FlashResponse, sbot::systemctl_sbot_cmd};
// ROUTE: /settings/scuttlebutt/stop // ROUTE: /settings/scuttlebutt/stop
@ -10,7 +10,7 @@ use crate::utils::{flash::FlashResponse, sbot};
/// the attempt via a flash message. /// the attempt via a flash message.
pub fn stop_sbot() -> Response { pub fn stop_sbot() -> Response {
info!("Stopping go-sbot.service"); info!("Stopping go-sbot.service");
let (flash_name, flash_msg) = match sbot::system_sbot_cmd("stop") { let (flash_name, flash_msg) = match systemctl_sbot_cmd("stop") {
Ok(_) => ( Ok(_) => (
"flash_name=success".to_string(), "flash_name=success".to_string(),
"flash_msg=Sbot process has been stopped".to_string(), "flash_msg=Sbot process has been stopped".to_string(),

View File

@ -1,238 +1,381 @@
use log::info; use std::process::Command;
use rocket::{
get,
request::FlashMessage,
response::{Flash, Redirect},
};
use rocket_dyn_templates::Template;
use serde::Serialize;
use std::{
io,
process::{Command, Output},
};
use peach_lib::{ use maud::{html, Markup, PreEscaped};
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus, use peach_lib::{config_manager, dyndns_client, oled_client};
};
use peach_stats::{ use peach_stats::{
stats, stats,
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat}, stats::{CpuStatPercentages, MemStat},
}; };
use crate::routes::authentication::Authenticated; use crate::{templates, utils::theme};
// HELPERS AND ROUTES FOR /status // ROUTE: /status
/// System statistics data. /// Query systemd to determine the state of the networking service.
#[derive(Debug, Serialize)] fn retrieve_networking_state() -> Option<String> {
pub struct StatusContext { // call: `systemctl show networking.service --no-page`
pub back: Option<String>, let networking_service_output = Command::new("systemctl")
pub cpu_stat_percent: Option<CpuStatPercentages>, .arg("show")
pub disk_stats: Vec<DiskUsage>, .arg("networking.service")
pub flash_name: Option<String>, .arg("--no-page")
pub flash_msg: Option<String>, .output()
pub load_average: Option<LoadAverage>, .ok()?;
pub mem_stats: Option<MemStat>,
pub network_ping: String, let service_info = std::str::from_utf8(&networking_service_output.stdout).ok()?;
pub oled_ping: String,
pub dyndns_enabled: bool, // find the line starting with "ActiveState=" and return the value
pub dyndns_is_online: bool, service_info
pub config_is_valid: bool, .lines()
pub sbot_is_online: bool, .find(|line| line.starts_with("ActiveState="))
pub title: Option<String>, .and_then(|line| line.strip_prefix("ActiveState="))
pub uptime: Option<i32>, .map(|state| state.to_string())
} }
impl StatusContext { /// Query systemd to determine the state of the sbot service.
pub fn build() -> StatusContext { fn retrieve_sbot_state() -> Option<String> {
// convert result to Option<CpuStatPercentages>, discard any error // retrieve the name of the go-sbot service or set default
let cpu_stat_percent = stats::cpu_stats_percent().ok(); let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE")
let load_average = stats::load_average().ok(); .unwrap_or_else(|_| "go-sbot.service".to_string());
let mem_stats = stats::mem_stats().ok();
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call let sbot_service_output = Command::new("systemctl")
// instead of: "is the network json-rpc server running?", we want to ask: .arg("show")
// "is the wpa_supplicant systemd service functioning correctly?" .arg(go_sbot_service)
let network_ping = match network_client::ping() { .arg("--no-page")
Ok(_) => "ONLINE".to_string(), .output()
Err(_) => "OFFLINE".to_string(), .ok()?;
};
let service_info = std::str::from_utf8(&sbot_service_output.stdout).ok()?;
// find the line starting with "ActiveState=" and return the value
service_info
.lines()
.find(|line| line.starts_with("ActiveState="))
.and_then(|line| line.strip_prefix("ActiveState="))
.map(|state| state.to_string())
}
fn retrieve_device_status_data() -> (Option<u64>, String) {
let uptime = stats::uptime().ok();
let oled_ping = match oled_client::ping() { let oled_ping = match oled_client::ping() {
Ok(_) => "ONLINE".to_string(), Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(), Err(_) => "OFFLINE".to_string(),
}; };
let uptime = match stats::uptime() { (uptime, oled_ping)
Ok(secs) => { }
let uptime_mins = secs / 60;
uptime_mins.to_string() fn retrieve_device_usage_data() -> (Option<CpuStatPercentages>, Option<MemStat>) {
// convert result to Option<CpuStatPercentages>, discard any error
let cpu_stat_percent = stats::cpu_stats_percent().ok();
let mem_stats = stats::mem_stats().ok();
(cpu_stat_percent, mem_stats)
}
fn render_network_capsule() -> Markup {
let (state, stack_class, img_class) = match retrieve_networking_state() {
Some(state) if state.as_str() == "active" => {
("active", "stack capsule border-success", "icon icon-medium")
} }
Err(_) => "Unavailable".to_string(), Some(state) if state.as_str() == "inactive" => (
"inactive",
"stack capsule border-warning",
"icon icon-inactive icon-medium",
),
Some(state) if state.as_str() == "failed" => (
"failed",
"stack capsule border-danger",
"icon icon-inactive icon-medium",
),
_ => (
"error",
"stack capsule border-danger",
"icon icon-inactive icon-medium",
),
}; };
// parse the uptime string to a signed integer (for math) html! {
let uptime_parsed = uptime.parse::<i32>().ok(); (PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
div class=(stack_class) {
img id="networkIcon" class=(img_class) alt="Network" title="Networking service status" src="icons/wifi.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Networking" }
label class="label-small font-near-black" { (state.to_uppercase()) }
}
}
}
}
// serialize disk usage data into Vec<DiskUsage> fn render_oled_capsule(state: String) -> Markup {
let (stack_class, img_class) = match state.as_str() {
"ONLINE" => ("stack capsule border-success", "icon icon-medium"),
_ => (
"stack capsule border-warning",
"icon icon-inactive icon-medium",
),
};
html! {
(PreEscaped("<!-- PEACH-OLED STATUS STACK -->"))
div class=(stack_class) {
img id="oledIcon" class=(img_class) alt="Display" title="OLED display microservice status" src="icons/lcd.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Display" }
label class="label-small font-near-black" { (state) }
}
}
}
}
fn render_diagnostics_capsule() -> Markup {
// TODO: write a diagnostics module (maybe in peach-lib)
let diagnostics_state = "CLEAR";
html! {
(PreEscaped("<!-- DIAGNOSTICS AND LOGS STACK -->"))
div class="stack capsule border-success" {
img id="statsIcon" class="icon icon-medium" alt="Line chart" title="System diagnostics and logs" src="icons/chart.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Diagnostics" }
label class="label-small font-near-black" { (diagnostics_state) };
}
}
}
}
fn render_dyndns_capsule() -> Markup {
let (state, stack_class, img_class) = match dyndns_client::is_dns_updater_online() {
Ok(true) => ("ONLINE", "stack capsule border-success", "icon icon-medium"),
Ok(false) => (
"OFFLINE",
"stack capsule border-warning",
"icon icon-inactive icon-medium",
),
Err(_) => (
"ERROR",
"stack capsule border-danger",
"icon icon-inactive icon-medium",
),
};
html! {
(PreEscaped("<!-- DYNDNS STATUS STACK -->"))
div class=(stack_class) {
img id="dnsIcon" class=(img_class) alt="Dyndns" title="Dyndns status" src="icons/dns.png";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Dyn DNS" }
label class="label-small font-near-black" { (state) }
}
}
}
}
fn render_config_capsule() -> Markup {
let (state, stack_class, img_class) =
match config_manager::load_peach_config_from_disc().is_ok() {
true => ("LOADED", "stack capsule border-success", "icon icon-medium"),
false => (
"INVALID",
"stack capsule border-warning",
"icon icon-inactive icon-medium",
),
};
html! {
(PreEscaped("<!-- CONFIG STATUS STACK -->"))
div class=(stack_class) {
img id="configIcon" class=(img_class) alt="Config" title="Config status" src="icons/clipboard.png";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Config" }
label class="label-small font-near-black" { (state) }
}
}
}
}
fn render_sbot_capsule() -> Markup {
let (state, stack_class, img_class) = match retrieve_sbot_state() {
Some(state) if state.as_str() == "active" => {
("active", "stack capsule border-success", "icon icon-medium")
}
Some(state) if state.as_str() == "inactive" => (
"inactive",
"stack capsule border-warning",
"icon icon-inactive icon-medium",
),
Some(state) if state.as_str() == "failed" => (
"failed",
"stack capsule border-danger",
"icon icon-inactive icon-medium",
),
_ => (
"error",
"stack capsule border-danger",
"icon icon-inactive icon-medium",
),
};
html! {
(PreEscaped("<!-- SBOT STATUS STACK -->"))
div class=(stack_class) {
img id="sbotIcon" class=(img_class) alt="Sbot" title="Sbot status" src="icons/hermies.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Sbot" }
label class="label-small font-near-black" { (state.to_uppercase()) }
}
}
}
}
fn render_cpu_usage_meter(cpu_usage_percent: Option<CpuStatPercentages>) -> Markup {
html! {
@if let Some(cpu_usage) = cpu_usage_percent {
@let cpu_usage_total = (cpu_usage.nice + cpu_usage.system + cpu_usage.user).round();
div class="flex-grid" {
span class="card-text" { "CPU" }
span class="label-small push-right" { (cpu_usage_total) "%" }
}
meter value=(cpu_usage_total) min="0" max="100" title="CPU usage" {
div class="meter-gauge" {
span style={ "width: " (cpu_usage_total) "%;" } {
"CPU Usage"
}
}
}
} @else {
p class="card-text" { "CPU usage data unavailable" }
}
}
}
fn render_mem_usage_meter(mem_stats: Option<MemStat>) -> Markup {
html! {
@if let Some(mem) = mem_stats {
// convert kilobyte values to megabyte values
@let mem_free_mb = mem.free / 1024;
@let mem_total_mb = mem.total / 1024;
@let mem_used_mb = mem.used / 1024;
// calculate memory usage as a percentage
@let mem_used_percent = mem_used_mb * 100 / mem_total_mb;
// render disk free value as megabytes or gigabytes based on size
@let mem_free_value = if mem_free_mb > 1024 {
format!("{} GB", (mem_free_mb / 1024))
} else {
format!("{} MB", mem_free_mb)
};
div class="flex-grid" {
span class="card-text" { "Memory" }
span class="label-small push-right" { (mem_used_percent) "% (" (mem_free_value) " free)" }
}
meter value=(mem_used_mb) min="0" max=(mem_total_mb) title="Memory usage" {
div class="meter-gauge" {
span style={ "width: " (mem_used_percent) "%;" } { "Memory Usage" }
}
}
} @else {
p class="card-text" { "Memory usage data unavailable" }
}
}
}
fn render_disk_usage_meter() -> Markup {
let disk_usage_stats = match stats::disk_usage() { let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => disks, Ok(disks) => disks,
Err(_) => Vec::new(), Err(_) => Vec::new(),
}; };
let mut disk_stats = Vec::new();
// select only the partition we're interested in: /dev/mmcblk0p2 ("/") // select only the partition we're interested in: /dev/mmcblk0p2 ("/")
for disk in disk_usage_stats { let disk_usage = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
if disk.mountpoint == "/" {
disk_stats.push(disk);
}
}
// dyndns_is_online & config_is_valid html! {
let dyndns_enabled: bool; @if let Some(disk) = disk_usage {
let dyndns_is_online: bool; // calculate free disk space in megabytes
let config_is_valid: bool; @let disk_free_mb = disk.one_k_blocks_free / 1024;
let load_peach_config_result = load_peach_config(); // calculate free disk space in gigabytes
match load_peach_config_result { @let disk_free_gb = disk_free_mb / 1024;
Ok(peach_config) => { // render disk free value as megabytes or gigabytes based on size
dyndns_enabled = peach_config.dyn_enabled; @let disk_free_value = if disk_free_mb > 1024 {
config_is_valid = true; format!("{} GB", disk_free_gb)
if dyndns_enabled {
let is_dyndns_online_result = dyndns_client::is_dns_updater_online();
match is_dyndns_online_result {
Ok(is_online) => {
dyndns_is_online = is_online;
}
Err(_err) => {
dyndns_is_online = false;
}
}
} else { } else {
dyndns_is_online = false; format!("{} MB", disk_free_mb)
}
}
Err(_err) => {
dyndns_enabled = false;
dyndns_is_online = false;
config_is_valid = false;
}
}
// test if go-sbot is running
let sbot_status = SbotStatus::read();
let sbot_is_online: bool = match sbot_status {
// return true if state is active
Ok(status) => matches!(status.state == Some("active".to_string()), true),
_ => false,
}; };
StatusContext { div class="flex-grid" {
back: None, span class="card-text" { "Disk" }
cpu_stat_percent, span class="label-small push-right" { (disk.used_percentage) "% (" (disk_free_value) " free)" }
disk_stats, }
flash_name: None, meter value=(disk.used_percentage) min="0" max="100" title="Disk usage" {
flash_msg: None, div class="meter-gauge" {
load_average, span style={ "width: " (disk.used_percentage) "%;" } {
mem_stats, "Disk Usage"
network_ping, }
oled_ping, }
dyndns_enabled, }
dyndns_is_online, } @else {
config_is_valid, p class="card-text" { "Disk usage data unavailable" }
sbot_is_online,
title: None,
uptime: uptime_parsed,
} }
} }
} }
#[get("/")] /// Display system uptime in hours and minutes.
pub fn device_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template { fn render_uptime_capsule(uptime: Option<u64>) -> Markup {
// assign context through context_builder call html! {
let mut context = StatusContext::build(); @if let Some(uptime_secs) = uptime {
context.back = Some("/".to_string()); @let uptime_mins = uptime_secs / 60;
context.title = Some("Device Status".to_string()); @if uptime_mins < 60 {
// check to see if there is a flash message to display // display system uptime in minutes
if let Some(flash) = flash { p class="capsule center-text" {
// add flash message contents to the context object "Uptime: " (uptime_mins) " minutes"
context.flash_name = Some(flash.kind().to_string()); }
context.flash_msg = Some(flash.message().to_string()); } @else {
// display system uptime in hours and minutes
@let hours = uptime_mins / 60;
@let mins = uptime_mins % 60;
p class="capsule center-text" {
"Uptime: " (hours) " hours, " (mins) " minutes"
}
}
} @else {
p class="card-text" { "Uptime data unavailable" }
}
}
}
/// Device status template builder.
pub fn build_template() -> PreEscaped<String> {
let (uptime, oled_state) = retrieve_device_status_data();
let (cpu_usage, mem_usage) = retrieve_device_usage_data();
let device_status_template = html! {
(PreEscaped("<!-- DEVICE STATUS CARD -->"))
div class="card center" {
div class="card-container" {
// display status capsules for network, oled and diagnostics
div class="three-grid" {
(render_network_capsule())
(render_oled_capsule(oled_state))
(render_diagnostics_capsule())
}
// display status capsules for dyndns, config and sbot
div class="three-grid" style="padding-top: 1rem; padding-bottom: 1rem;" {
(render_dyndns_capsule())
(render_config_capsule())
(render_sbot_capsule())
}
(render_cpu_usage_meter(cpu_usage))
(render_mem_usage_meter(mem_usage))
(render_disk_usage_meter())
(render_uptime_capsule(uptime))
}
}
}; };
// template_dir is set in Rocket.toml
Template::render("status/device", &context) let body = templates::nav::build_template(device_status_template, "Device Status", Some("/"));
}
let theme = theme::get_theme();
// HELPERS AND ROUTES FOR /power/reboot
templates::base::build_template(body, theme)
/// Executes a system command to reboot the device immediately.
pub fn reboot() -> io::Result<Output> {
info!("Rebooting the device");
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
// response but this is not possible with the `shutdown` command alone.
// TODO: send "rebooting..." message to `peach-oled` for display
Command::new("sudo")
.arg("shutdown")
.arg("-r")
.arg("now")
.output()
}
#[get("/power/reboot")]
pub fn reboot_cmd(_auth: Authenticated) -> Flash<Redirect> {
match reboot() {
Ok(_) => Flash::success(Redirect::to("/power"), "Rebooting the device"),
Err(_) => Flash::error(Redirect::to("/power"), "Failed to reboot the device"),
}
}
// HELPERS AND ROUTES FOR /power/shutdown
/// Executes a system command to shutdown the device immediately.
pub fn shutdown() -> io::Result<Output> {
info!("Shutting down the device");
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
// response but this is not possible with the `shutdown` command alone.
// TODO: send "shutting down..." message to `peach-oled` for display
Command::new("sudo").arg("shutdown").arg("now").output()
}
#[get("/power/shutdown")]
pub fn shutdown_cmd(_auth: Authenticated) -> Flash<Redirect> {
match shutdown() {
Ok(_) => Flash::success(Redirect::to("/power"), "Shutting down the device"),
Err(_) => Flash::error(Redirect::to("/power"), "Failed to shutdown the device"),
}
}
// HELPERS AND ROUTES FOR /power
#[derive(Debug, Serialize)]
pub struct PowerContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl PowerContext {
pub fn build() -> PowerContext {
PowerContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/power")]
pub fn power_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = PowerContext::build();
context.back = Some("/".to_string());
context.title = Some("Power Menu".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("power", &context)
} }

View File

@ -1,3 +1,3 @@
//pub mod device; pub mod device;
//pub mod network; pub mod network;
pub mod scuttlebutt; pub mod scuttlebutt;

View File

@ -1,21 +1,285 @@
use rocket::{get, request::FlashMessage}; use maud::{html, Markup, PreEscaped};
use rocket_dyn_templates::Template; use peach_network::network;
use vnstat_parse::Vnstat;
use crate::context::network::NetworkStatusContext; use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /status/network enum NetworkState {
AccessPoint,
WiFiClient,
}
#[get("/network")] // ROUTE: /status/network
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkStatusContext::build();
context.back = Some("/status".to_string());
context.title = Some("Network Status".to_string());
if let Some(flash) = flash { /// Render the cog icon which is used as a link to the network settings page.
context.flash_name = Some(flash.kind().to_string()); fn render_network_config_icon() -> Markup {
context.flash_msg = Some(flash.message().to_string()); html! {
(PreEscaped("<!-- top-right config icon -->"))
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure";
}
}
}
/// Render the network mode icon, either a WiFi signal or router, based
/// on the state of the AP and WiFi interfaces.
///
/// A router icon is shown if the AP is online (interface is "up").
///
/// A WiFi signal icon is shown if the AP interface is down. The colour of
/// the icon is black if the WLAN interface is up and gray if it's down.
fn render_network_mode_icon(state: &NetworkState) -> Markup {
// TODO: make this DRYer
let (icon_class, icon_src, icon_alt, label_title, label_value) = match state {
NetworkState::AccessPoint => (
"center icon icon-active",
"/icons/router.svg",
"WiFi router",
"Access Point Online",
"ONLINE",
),
NetworkState::WiFiClient => match network::state(WLAN_IFACE) {
Ok(Some(state)) if state == "up" => (
"center icon icon-active",
"/icons/wifi.svg",
"WiFi signal",
"WiFi Client Online",
"ONLINE",
),
_ => (
"center icon icon-inactive",
"/icons/wifi.svg",
"WiFi signal",
"WiFi Client Offline",
"OFFLINE",
),
},
}; };
Template::render("status/network", &context) html! {
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class=(icon_class) src=(icon_src) alt=(icon_alt);
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title=(label_title) { (label_value) }
}
}
}
/// Render the network data associated with the deployed access point or
/// connected WiFi client depending on active mode.
///
/// Data includes the network mode (access point or WiFi client), SSID and IP
/// address.
fn render_network_data(state: &NetworkState, ssid: String, ip: String) -> Markup {
let (mode_value, mode_title, ssid_value, ip_title) = match state {
NetworkState::AccessPoint => (
"Access Point",
"Access Point SSID",
// TODO: remove hardcoding of this value (query interface instead)
"peach",
"Access Point IP Address",
),
NetworkState::WiFiClient => (
"WiFi Client",
"WiFi SSID",
ssid.as_str(),
"WiFi Client IP Address",
),
};
html! {
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
div class="grid-column-2" {
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { (mode_value) }
label class="label-small font-gray" for="netSsid" title=(mode_title) { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (ssid_value) }
label class="label-small font-gray" for="netIp" title=(ip_title) { "IP" }
p id="netIp" class="card-text" title="IP" { (ip) }
}
}
}
/// Render the network status grid comprised of the network config icon,
/// network mode icon and network data text.
fn render_network_status_grid(state: &NetworkState, ssid: String, ip: String) -> Markup {
html! {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div class="two-grid" title="PeachCloud network mode and status" {
(render_network_config_icon())
(PreEscaped("<!-- left column -->"))
(render_network_mode_icon(state))
(PreEscaped("<!-- right column -->"))
(render_network_data(state, ssid, ip))
}
}
}
/// Render the signal strength stack comprised of a signal icon, RSSI value
/// and label.
///
/// This stack is displayed when the network mode is set to WiFi
/// client (ie. the value reported is the strength of the connection of the
/// local WiFi interface to a remote access point).
fn render_signal_strength_stack() -> Markup {
let wlan_rssi = match network::rssi(WLAN_IFACE) {
Ok(Some(rssi)) => rssi,
_ => 0.to_string(),
};
html! {
div class="stack" {
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/icons/low-signal.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" { (wlan_rssi) }
}
label class="label-small font-gray" { "SIGNAL" }
}
}
}
/// Render the connected devices stack comprised of a devices icon, value
/// of connected devices and label.
///
/// This stack is displayed when the network mode is set to access point
/// (ie. the value reported is the number of remote devices connected to the
/// local access point).
fn render_connected_devices_stack() -> Markup {
html! {
div class="stack" {
img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices";
div class="flex-grid" style="padding-top: 0.5rem;" {
label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices";
}
label class="label-small font-gray" { "DEVICES" }
}
}
}
/// Render the data download stack comprised of a download icon, traffic value
/// and label.
///
/// A zero value is displayed if no interface traffic is available for the
/// WLAN interface.
fn render_data_download_stack(iface_traffic: &Option<Vnstat>) -> Markup {
html! {
div class="stack" {
img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = iface_traffic {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title={ "Data download total in " (traffic.all_time_rx_unit) } { (traffic.all_time_rx) }
label class="label-small font-near-black" { (traffic.all_time_rx_unit) }
} @else {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
label class="label-small font-near-black";
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
}
}
/// Render the data upload stack comprised of an upload icon, traffic value
/// and label.
///
/// A zero value is displayed if no interface traffic is available for the
/// WLAN interface.
fn render_data_upload_stack(iface_traffic: Option<Vnstat>) -> Markup {
html! {
div class="stack" {
img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = iface_traffic {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title={ "Data upload total in " (traffic.all_time_tx_unit) } { (traffic.all_time_tx) }
label class="label-small font-near-black" { (traffic.all_time_tx_unit) }
} @else {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
label class="label-small font-near-black";
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
/// Render the device / signal and traffic grid.
///
/// The connected devices stack is displayed if the network mode is set to
/// access point and the signal strength stack is displayed if the network
/// mode is set to WiFi client.
fn render_device_and_traffic_grid(state: NetworkState, iface_traffic: Option<Vnstat>) -> Markup {
html! {
div class="three-grid card-container" {
@match state {
NetworkState::AccessPoint => (render_connected_devices_stack()),
NetworkState::WiFiClient => (render_signal_strength_stack()),
}
(render_data_download_stack(&iface_traffic))
(render_data_upload_stack(iface_traffic))
}
}
}
/// Network state data retrieval.
///
/// This data is injected into the template rendering functions.
fn retrieve_network_data() -> (NetworkState, Option<Vnstat>, String, String) {
// if the access point interface is "up",
// retrieve the traffic stats, ip and ssidfor the ap interface.
// otherwise retrieve the stats and ip for the wlan interface.
let (state, traffic, ip, ssid) = match network::state(AP_IFACE) {
Ok(Some(state)) if state == "up" => {
let ap_traffic = Vnstat::get(AP_IFACE).ok();
let ap_ip = match network::ip(AP_IFACE) {
Ok(Some(ip)) => ip,
_ => String::from("x.x.x.x"),
};
let ap_ssid = String::from("peach");
(NetworkState::AccessPoint, ap_traffic, ap_ip, ap_ssid)
}
_ => {
let wlan_traffic = Vnstat::get(WLAN_IFACE).ok();
let wlan_ip = match network::ip(WLAN_IFACE) {
Ok(Some(ip)) => ip,
_ => String::from("x.x.x.x"),
};
let wlan_ssid = match network::ssid(WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => String::from("Not connected"),
};
(NetworkState::WiFiClient, wlan_traffic, wlan_ip, wlan_ssid)
}
};
(state, traffic, ip, ssid)
}
/// Network status template builder.
pub fn build_template() -> PreEscaped<String> {
let (state, traffic, ip, ssid) = retrieve_network_data();
let network_status_template = html! {
(PreEscaped("<!-- NETWORK STATUS CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class="capsule capsule-container success-border" {
(render_network_status_grid(&state, ssid, ip))
hr style="color: var(--light-gray);";
(render_device_and_traffic_grid(state, traffic))
}
}
};
let body =
templates::nav::build_template(network_status_template, "Network Status", Some("/status"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
} }

View File

@ -14,7 +14,7 @@ pub fn build_template(
let theme = theme::get_theme(); let theme = theme::get_theme();
// conditionally render the hermies icon and theme-switcher icon with correct link // conditionally render the hermies icon and theme-switcher icon with correct link
let (hermies, switcher) = match theme.as_str() { let (hermies, theme_switcher) = match theme.as_str() {
// if we're using the dark theme, render light icons and "light" query param // if we're using the dark theme, render light icons and "light" query param
"dark" => ( "dark" => (
"/icons/hermies_hex_light.svg", "/icons/hermies_hex_light.svg",
@ -56,8 +56,7 @@ pub fn build_template(
a class="nav-item" href="/" { a class="nav-item" href="/" {
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home"; img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
} }
// render the pre-defined theme-switcher icon (theme_switcher)
(switcher)
} }
} }
} }

View File

@ -11,71 +11,36 @@ use std::{
use async_std::task; use async_std::task;
use dirs; use dirs;
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use golgi::{
api::friends::RelationshipQuery, blobs, messages::SsbMessageKVT, sbot::Keystore, Sbot,
};
use log::debug; use log::debug;
use peach_lib::config_manager; use peach_lib::config_manager;
use peach_lib::sbot::SbotConfig; use peach_lib::sbot::SbotConfig;
use peach_lib::sbot::init_sbot;
use peach_lib::ssb_messages::SsbMessageKVT;
use rouille::input::post::BufferedFile; use rouille::input::post::BufferedFile;
use temporary::Directory; use temporary::Directory;
use peach_lib::serde_json::json;
use peach_lib::tilde_client::TildeClient;
use crate::{error::PeachWebError, utils::sbot}; use crate::{error::PeachWebError, utils::sbot};
// SBOT HELPER FUNCTIONS // SBOT HELPER FUNCTIONS
/// On non-docker based deployments (peachcloud, yunohost), we use systemctl /// Executes a systemctl command for the solar-sbot.service process.
/// On docker-based deployments, we use supervisord pub fn systemctl_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
/// This utility function calls the correct system calls based on these parameters.
pub fn system_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
let system_manager = config_manager::get_config_value("SYSTEM_MANAGER")?;
match system_manager.as_str() {
"systemd" => {
let output = Command::new("sudo") let output = Command::new("sudo")
.arg("systemctl") .arg("systemctl")
.arg(cmd) .arg(cmd)
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?) .arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?; .output()?;
Ok(output) Ok(output)
}
"supervisord" => {
match cmd {
"enable" => {
// TODO: implement this
let output = Command::new("echo")
.arg("implement this (enable)")
.output()?;
Ok(output)
}
"disable" => {
let output = Command::new("echo")
.arg("implement this (disable)")
.output()?;
Ok(output)
}
_ => {
let output = Command::new("supervisorctl")
.arg(cmd)
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.output()?;
Ok(output)
}
}
}
_ => Err(PeachWebError::System(format!(
"Invalid configuration for SYSTEM_MANAGER: {:?}",
system_manager
))),
}
} }
/// Executes a system stop command followed by start command. /// Executes a systemctl stop command followed by start command.
/// Returns a redirect with a flash message stating the output of the restart attempt. /// Returns a redirect with a flash message stating the output of the restart attempt.
pub fn restart_sbot_process() -> (String, String) { pub fn restart_sbot_process() -> (String, String) {
debug!("Restarting go-sbot.service"); debug!("Restarting solar-sbot.service");
match system_sbot_cmd("stop") { match systemctl_sbot_cmd("stop") {
// if stop was successful, try to start the process // if stop was successful, try to start the process
Ok(_) => match system_sbot_cmd("start") { Ok(_) => match systemctl_sbot_cmd("start") {
Ok(_) => ( Ok(_) => (
"success".to_string(), "success".to_string(),
"Updated configuration and restarted the sbot process".to_string(), "Updated configuration and restarted the sbot process".to_string(),
@ -99,23 +64,14 @@ pub fn restart_sbot_process() -> (String, String) {
} }
/// Initialise an sbot client with the given configuration parameters. /// Initialise an sbot client with the given configuration parameters.
pub async fn init_sbot_with_config( pub async fn init_sbot_client() -> Result<TildeClient, PeachWebError> {
sbot_config: &Option<SbotConfig>,
) -> Result<Sbot, PeachWebError> {
debug!("Initialising an sbot client with configuration parameters"); debug!("Initialising an sbot client with configuration parameters");
// initialise sbot connection with ip:port and shscap from config file // initialise sbot connection with ip:port and shscap from config file
let key_path = format!( let key_path = format!(
"{}/secret", "{}/secret.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")? config_manager::get_config_value("TILDE_SBOT_DATADIR")?
); );
let sbot_client = match sbot_config { let sbot_client = init_sbot().await?;
// TODO: panics if we pass `Some(conf.shscap)` as second arg
Some(conf) => {
let ip_port = conf.lis.clone();
Sbot::init(Keystore::CustomGoSbot(key_path), Some(ip_port), None).await?
}
None => Sbot::init(Keystore::CustomGoSbot(key_path), None, None).await?,
};
Ok(sbot_client) Ok(sbot_client)
} }
@ -158,49 +114,53 @@ pub fn validate_public_key(public_key: &str) -> Result<(), String> {
/// reverses the list and reads the sequence number of the most recently /// reverses the list and reads the sequence number of the most recently
/// authored message. This gives us the size of the database in terms of /// authored message. This gives us the size of the database in terms of
/// the total number of locally-authored messages. /// the total number of locally-authored messages.
pub fn latest_sequence_number() -> Result<u64, Box<dyn Error>> { pub fn latest_sequence_number() -> Result<u64, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
Err(PeachWebError::NotYetImplemented)
// retrieve the local id // retrieve the local id
let id = sbot_client.whoami().await?; // let id = sbot_client.whoami().await?;
let history_stream = sbot_client.create_history_stream(id).await?; // let history_stream = sbot_client.feed(&id).await?;
let mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
// there will be zero messages when the sbot is run for the first time // let mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
if msgs.is_empty() { //
Ok(0) // // there will be zero messages when the sbot is run for the first time
} else { // if msgs.is_empty() {
// reverse the list of messages so we can easily reference the latest one // Ok(0)
msgs.reverse(); // } else {
// // reverse the list of messages so we can easily reference the latest one
// return the sequence number of the latest msg // msgs.reverse();
Ok(msgs[0].value.sequence) //
} // // return the sequence number of the latest msg
// Ok(msgs[0].value.sequence)
// }
}) })
} }
pub fn create_invite(uses: u16) -> Result<String, Box<dyn Error>> { pub fn create_invite(uses: u16) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
debug!("Generating Scuttlebutt invite code"); debug!("Generating Scuttlebutt invite code");
let mut invite_code = sbot_client.invite_create(uses).await?; Err(PeachWebError::NotYetImplemented)
// let mut invite_code = sbot_client.invite_create(uses).await?;
// insert domain into invite if one is configured //
let domain = config_manager::get_config_value("EXTERNAL_DOMAIN")?; // // insert domain into invite if one is configured
if !domain.is_empty() { // let domain = config_manager::get_config_value("EXTERNAL_DOMAIN")?;
invite_code = domain + &invite_code[4..]; // if !domain.is_empty() {
} // invite_code = domain + &invite_code[4..];
// }
Ok(invite_code) //
// Ok(invite_code)
}) })
} }
@ -240,11 +200,9 @@ impl Profile {
/// Retrieve the profile info for the given public key. /// Retrieve the profile info for the given public key.
pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> { pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let sbot_client = init_sbot_client().await?;
let local_id = sbot_client.whoami().await?; let local_id = sbot_client.whoami().await?;
@ -256,32 +214,11 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
// we are not dealing with the local profile // we are not dealing with the local profile
profile.is_local_profile = false; profile.is_local_profile = false;
// determine relationship between peer and local id
let follow_query = RelationshipQuery {
source: local_id.clone(),
dest: peer_id.clone(),
};
// query follow state // query follow state
profile.following = match sbot_client.friends_is_following(follow_query).await { profile.following = Some(sbot_client.is_following(&local_id, &peer_id).await?);
Ok(following) if following == "true" => Some(true),
Ok(following) if following == "false" => Some(false),
_ => None,
};
// TODO: i don't like that we have to instantiate the same query object // TODO: implement this check in solar_client so that this can be a real value
// twice. see if we can streamline this in golgi profile.blocking = Some(false);
let block_query = RelationshipQuery {
source: local_id.clone(),
dest: peer_id.clone(),
};
// query block state
profile.blocking = match sbot_client.friends_is_blocking(block_query).await {
Ok(blocking) if blocking == "true" => Some(true),
Ok(blocking) if blocking == "false" => Some(false),
_ => None,
};
peer_id peer_id
} else { } else {
@ -292,7 +229,7 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
}; };
// retrieve the profile info for the given id // retrieve the profile info for the given id
let info = sbot_client.get_profile_info(&id).await?; let info = get_peer_info(&id).await?;
// set each profile field accordingly // set each profile field accordingly
for (key, val) in info { for (key, val) in info {
match key.as_str() { match key.as_str() {
@ -302,26 +239,27 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
_ => (), _ => (),
} }
} }
//
// assign the ssb public key // assign the ssb public key
// (could be for the local profile or a peer) // (could be for the local profile or a peer)
profile.id = Some(id); profile.id = Some(id);
// determine the path to the blob defined by the value of `profile.image` // TODO: blobs support
if let Some(ref blob_id) = profile.image { // // determine the path to the blob defined by the value of `profile.image`
profile.blob_path = match blobs::get_blob_path(blob_id) { // if let Some(ref blob_id) = profile.image {
Ok(path) => { // profile.blob_path = match blobs::get_blob_path(blob_id) {
// if we get the path, check if the blob is in the blobstore. // Ok(path) => {
// this allows us to default to a placeholder image in the template // // if we get the path, check if the blob is in the blobstore.
if let Ok(exists) = blob_is_stored_locally(&path).await { // // this allows us to default to a placeholder image in the template
profile.blob_exists = exists // if let Ok(exists) = blob_is_stored_locally(&path).await {
}; // profile.blob_exists = exists
// };
Some(path) //
} // Some(path)
Err(_) => None, // }
} // Err(_) => None,
} // }
// }
Ok(profile) Ok(profile)
}) })
@ -336,123 +274,119 @@ pub fn update_profile_info(
new_name: Option<String>, new_name: Option<String>,
new_description: Option<String>, new_description: Option<String>,
image: Option<BufferedFile>, image: Option<BufferedFile>,
) -> Result<String, String> { ) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await?;
.map_err(|e| e.to_string())?;
// track whether the name, description or image have been updated Err(PeachWebError::NotYetImplemented)
let mut name_updated: bool = false; // // track whether the name, description or image have been updated
let mut description_updated: bool = false; // let mut name_updated: bool = false;
let mut image_updated: bool = false; // let mut description_updated: bool = false;
// let mut image_updated: bool = false;
// check if a new_name value has been submitted in the form //
if let Some(name) = new_name { // // check if a new_name value has been submitted in the form
// only update the name if it has changed // if let Some(name) = new_name {
if name != current_name { // // only update the name if it has changed
debug!("Publishing a new Scuttlebutt profile name"); // if name != current_name {
if let Err(e) = sbot_client.publish_name(&name).await { // debug!("Publishing a new Scuttlebutt profile name");
return Err(format!("Failed to update name: {}", e)); // if let Err(e) = sbot_client.publish_name(&name).await {
} else { // return Err(format!("Failed to update name: {}", e));
name_updated = true // } else {
} // name_updated = true
} // }
} // }
// }
if let Some(description) = new_description { //
// only update the description if it has changed // if let Some(description) = new_description {
if description != current_description { // // only update the description if it has changed
debug!("Publishing a new Scuttlebutt profile description"); // if description != current_description {
if let Err(e) = sbot_client.publish_description(&description).await { // debug!("Publishing a new Scuttlebutt profile description");
return Err(format!("Failed to update description: {}", e)); // if let Err(e) = sbot_client.publish_description(&description).await {
} else { // return Err(format!("Failed to update description: {}", e));
description_updated = true // } else {
} // description_updated = true
} // }
} // }
// }
// only update the image if a file was uploaded //
if let Some(img) = image { // // only update the image if a file was uploaded
// only write the blob if it has a filename and data > 0 bytes // if let Some(img) = image {
if img.filename.is_some() && !img.data.is_empty() { // // only write the blob if it has a filename and data > 0 bytes
match write_blob_to_store(img).await { // if img.filename.is_some() && !img.data.is_empty() {
Ok(blob_id) => { // match write_blob_to_store(img).await {
// if the file was successfully added to the blobstore, // Ok(blob_id) => {
// publish an about image message with the blob id // // if the file was successfully added to the blobstore,
if let Err(e) = sbot_client.publish_image(&blob_id).await { // // publish an about image message with the blob id
return Err(format!("Failed to update image: {}", e)); // if let Err(e) = sbot_client.publish_image(&blob_id).await {
} else { // return Err(format!("Failed to update image: {}", e));
image_updated = true // } else {
} // image_updated = true
} // }
Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)), // }
} // Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)),
} else { // }
image_updated = false // } else {
} // image_updated = false
} // }
// }
if name_updated || description_updated || image_updated { //
Ok("Profile updated".to_string()) // if name_updated || description_updated || image_updated {
} else { // Ok("Profile updated".to_string())
// no updates were made but no errors were encountered either // } else {
Ok("Profile info unchanged".to_string()) // // no updates were made but no errors were encountered either
} // Ok("Profile info unchanged".to_string())
// }
}) })
} }
/// Follow a peer. /// Follow a peer.
pub fn follow_peer(public_key: &str) -> Result<String, String> { pub fn follow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await?;
.map_err(|e| e.to_string())?;
debug!("Following a Scuttlebutt peer"); debug!("Following a Scuttlebutt peer");
match sbot_client.follow(public_key).await { Err(PeachWebError::NotYetImplemented)
Ok(_) => Ok("Followed peer".to_string()),
Err(e) => Err(format!("Failed to follow peer: {}", e)),
}
}) })
} }
/// Unfollow a peer. /// Unfollow a peer.
pub fn unfollow_peer(public_key: &str) -> Result<String, String> { pub fn unfollow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await?;
.map_err(|e| e.to_string())?;
debug!("Unfollowing a Scuttlebutt peer"); Err(PeachWebError::NotYetImplemented)
match sbot_client.unfollow(public_key).await { // debug!("Unfollowing a Scuttlebutt peer");
Ok(_) => Ok("Unfollowed peer".to_string()), // match sbot_client.unfollow(public_key).await {
Err(e) => Err(format!("Failed to unfollow peer: {}", e)), // Ok(_) => Ok("Unfollowed peer".to_string()),
} // Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
// }
}) })
} }
/// Block a peer. /// Block a peer.
pub fn block_peer(public_key: &str) -> Result<String, String> { pub fn block_peer(public_key: &str) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
debug!("Blocking a Scuttlebutt peer"); debug!("Blocking a Scuttlebutt peer");
match sbot_client.block(public_key).await { match sbot_client.create_block(public_key).await {
Ok(_) => Ok("Blocked peer".to_string()), Ok(_) => Ok("Blocked peer".to_string()),
Err(e) => Err(format!("Failed to block peer: {}", e)), Err(e) => Err(format!("Failed to block peer: {}", e)),
} }
@ -460,176 +394,159 @@ pub fn block_peer(public_key: &str) -> Result<String, String> {
} }
/// Unblock a peer. /// Unblock a peer.
pub fn unblock_peer(public_key: &str) -> Result<String, String> { pub fn unblock_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await?;
.map_err(|e| e.to_string())?;
debug!("Unblocking a Scuttlebutt peer"); debug!("Unblocking a Scuttlebutt peer");
match sbot_client.unblock(public_key).await { Err(PeachWebError::NotYetImplemented)
Ok(_) => Ok("Unblocked peer".to_string()), // match sbot_client.unblock(public_key).await {
Err(e) => Err(format!("Failed to unblock peer: {}", e)), // Ok(_) => Ok("Unblocked peer".to_string()),
} // Err(e) => Err(format!("Failed to unblock peer: {}", e)),
// }
}) })
} }
/// Retrieve a list of peers blocked by the local public key. /// Retrieve a list of peers blocked by the local public key.
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> { pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters // populate this vec to return
let sbot_config = SbotConfig::read().ok(); let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
let blocks = sbot_client.get_blocks().await?; let self_id = sbot_client.whoami().await?;
// we'll use this to store the profile info for each peer whom we block let blocks = sbot_client.get_blocks(&self_id).await?;
let mut peer_list = Vec::new();
if !blocks.is_empty() { if !blocks.is_empty() {
for peer in blocks.iter() { for peer in blocks.iter() {
// trim whitespace (including newline characters) and // trim whitespace (including newline characters) and
// remove the inverted-commas around the id // remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', ""); let key = peer.trim().replace('"', "");
// retrieve the profile info for the given peer let peer_info = get_peer_info(&key).await?;
let mut peer_info = sbot_client.get_profile_info(&key).await?;
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), key.to_string());
// we do not even attempt to find the blob for a blocked peer,
// since it may be vulgar to cause distress to the local peer.
peer_info.insert("blob_exists".to_string(), "false".to_string());
// push profile info to peer_list vec // push profile info to peer_list vec
peer_list.push(peer_info) to_return.push(peer_info)
} }
} }
// return the list of blocked peers // return the list of peers
Ok(peer_list) Ok(to_return)
}) })
} }
pub async fn get_peer_info(key: &str) -> Result<HashMap<String, String>, Box<dyn Error>> {
let mut sbot_client = init_sbot_client().await?;
// key,value dict of info about this peer
let mut peer_info = HashMap::new();
// retrieve the profile info for the given peer
// TODO: get all profile info not just latest_name
// TODO: latest_name throws an error
// TODO: just show as "error" isntead of aborting, if some field doesn't fetch
// let latest_name = sbot_client.latest_name(&key).await?;
let latest_name = "latest name".to_string();
if let Some(latest_description) = sbot_client.latest_description(&key).await.ok() {
peer_info.insert("description".to_string(), latest_description);
}
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), key.to_string());
peer_info.insert("name".to_string(), latest_name);
// retrieve the profile image blob id for the given peer
// TODO: blob support
// if let Some(blob_id) = peer_info.get("image") {
// // look-up the path for the image blob
// if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
// // insert the image blob path of the peer into the info hashmap
// peer_info.insert("blob_path".to_string(), blob_path.to_string());
// // check if the blob is in the blobstore
// // set a flag in the info hashmap
// match blob_is_stored_locally(&blob_path).await {
// Ok(exists) if exists => {
// peer_info.insert("blob_exists".to_string(), "true".to_string())
// }
// _ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
// };
// }
// }
Ok(peer_info)
}
/// Retrieve a list of peers followed by the local public key. /// Retrieve a list of peers followed by the local public key.
pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> { pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); // populate this vec to return
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
let follows = sbot_client.get_follows().await?; let self_id = sbot_client.whoami().await?;
// we'll use this to store the profile info for each peer who follows us let follows = sbot_client.get_follows(&self_id).await?;
let mut peer_list = Vec::new();
if !follows.is_empty() { if !follows.is_empty() {
for peer in follows.iter() { for peer in follows.iter() {
// trim whitespace (including newline characters) and // trim whitespace (including newline characters) and
// remove the inverted-commas around the id // remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', ""); let key = peer.trim().replace('"', "");
// retrieve the profile info for the given peer let peer_info = get_peer_info(&key).await?;
let mut peer_info = sbot_client.get_profile_info(&key).await?;
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), key.to_string());
// retrieve the profile image blob id for the given peer
if let Some(blob_id) = peer_info.get("image") {
// look-up the path for the image blob
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
// insert the image blob path of the peer into the info hashmap
peer_info.insert("blob_path".to_string(), blob_path.to_string());
// check if the blob is in the blobstore
// set a flag in the info hashmap
match blob_is_stored_locally(&blob_path).await {
Ok(exists) if exists => {
peer_info.insert("blob_exists".to_string(), "true".to_string())
}
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
};
}
}
// push profile info to peer_list vec // push profile info to peer_list vec
peer_list.push(peer_info) to_return.push(peer_info)
} }
} }
// return the list of peers // return the list of peers
Ok(peer_list) Ok(to_return)
}) })
} }
/// Retrieve a list of peers friended by the local public key. /// Retrieve a list of peers friended by the local public key.
pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> { pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); // populate this vec to return
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
let local_id = sbot_client.whoami().await?; let self_id = sbot_client.whoami().await?;
let follows = sbot_client.get_follows().await?; let friends = sbot_client.get_friends(&self_id).await?;
// we'll use this to store the profile info for each friend if !friends.is_empty() {
let mut peer_list = Vec::new(); for peer in friends.iter() {
if !follows.is_empty() {
for peer in follows.iter() {
// trim whitespace (including newline characters) and // trim whitespace (including newline characters) and
// remove the inverted-commas around the id // remove the inverted-commas around the id
let peer_id = peer.trim().replace('"', ""); // TODO: is this necessary?
// retrieve the profile info for the given peer let key = peer.trim().replace('"', "");
let mut peer_info = sbot_client.get_profile_info(&peer_id).await?; let peer_info = get_peer_info(&key).await?;
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), peer_id.to_string());
// retrieve the profile image blob id for the given peer
if let Some(blob_id) = peer_info.get("image") {
// look-up the path for the image blob
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
// insert the image blob path of the peer into the info hashmap
peer_info.insert("blob_path".to_string(), blob_path.to_string());
// check if the blob is in the blobstore
// set a flag in the info hashmap
match sbot::blob_is_stored_locally(&blob_path).await {
Ok(exists) if exists => {
peer_info.insert("blob_exists".to_string(), "true".to_string())
}
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
};
}
}
// check if the peer follows us (making us friends) // push profile info to peer_list vec
let follow_query = RelationshipQuery { to_return.push(peer_info)
source: peer_id.to_string(),
dest: local_id.clone(),
};
// query follow state
match sbot_client.friends_is_following(follow_query).await {
Ok(following) if following == "true" => {
// only push profile info to peer_list vec if they follow us
peer_list.push(peer_info)
}
_ => (),
};
} }
} }
// return the list of peers // return the list of peers
Ok(peer_list) Ok(to_return)
}) })
} }
/// Retrieve the local public key (id). /// Retrieve the local public key (id).
pub fn get_local_id() -> Result<String, Box<dyn Error>> { pub fn get_local_id() -> Result<String, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?; let mut sbot_client = init_sbot_client().await?;
let local_id = sbot_client.whoami().await?; let local_id = sbot_client.whoami().await?;
@ -639,16 +556,20 @@ pub fn get_local_id() -> Result<String, Box<dyn Error>> {
/// Publish a public post. /// Publish a public post.
pub fn publish_public_post(text: String) -> Result<String, String> { pub fn publish_public_post(text: String) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
debug!("Publishing a new Scuttlebutt public post"); debug!("Publishing a new Scuttlebutt public post");
match sbot_client.publish_post(&text).await { let post = json!({
"type": "post",
"text": &text,
});
match sbot_client.publish(post).await {
Ok(_) => Ok("Published post".to_string()), Ok(_) => Ok("Published post".to_string()),
Err(e) => Err(format!("Failed to publish post: {}", e)), Err(e) => Err(format!("Failed to publish post: {}", e)),
} }
@ -656,23 +577,23 @@ pub fn publish_public_post(text: String) -> Result<String, String> {
} }
/// Publish a private message. /// Publish a private message.
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, String> { pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, PeachWebError> {
// retrieve latest go-sbot configuration parameters // retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok(); let sbot_config = SbotConfig::read().ok();
task::block_on(async { task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config) let mut sbot_client = init_sbot_client()
.await .await?;
.map_err(|e| e.to_string())?;
debug!("Publishing a new Scuttlebutt private message"); Err(PeachWebError::NotYetImplemented)
match sbot_client // debug!("Publishing a new Scuttlebutt private message");
.publish_private(text.to_string(), recipients) // match sbot_client
.await // .publish_private(text.to_string(), recipients)
{ // .await
Ok(_) => Ok("Published private message".to_string()), // {
Err(e) => Err(format!("Failed to publish private message: {}", e)), // Ok(_) => Ok("Published private message".to_string()),
} // Err(e) => Err(format!("Failed to publish private message: {}", e)),
// }
}) })
} }
@ -726,20 +647,23 @@ pub async fn write_blob_to_store(image: BufferedFile) -> Result<String, PeachWeb
let mut buffer = Vec::new(); let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?; file.read_to_end(&mut buffer)?;
// hash the bytes representing the file Err(PeachWebError::NotYetImplemented)
let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?; // TODO: not yet implemented
// define the blobstore path and blob filename // // hash the bytes representing the file
let (blob_dir, blob_filename) = hex_hash.split_at(2); // let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
let go_ssb_path = get_go_ssb_path()?; //
let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir); // // define the blobstore path and blob filename
// let (blob_dir, blob_filename) = hex_hash.split_at(2);
// create the blobstore sub-directory // let go_ssb_path = get_go_ssb_path()?;
fs::create_dir_all(&blobstore_sub_dir)?; // let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir);
//
// copy the file to the blobstore // // create the blobstore sub-directory
let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename); // fs::create_dir_all(&blobstore_sub_dir)?;
fs::copy(temp_path, blob_path)?; //
// // copy the file to the blobstore
Ok(blob_id) // let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
// fs::copy(temp_path, blob_path)?;
//
// Ok(blob_id)
} }

2
tilde-client/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

12
tilde-client/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "tilde-client"
version = "0.0.1"
authors = ["Max Fowler <max@mfowler.info>"]
edition = "2018"
[dependencies]
async-std = "1.10"
anyhow = "1.0.86"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"

37
tilde-client/README.md Normal file
View File

@ -0,0 +1,37 @@
# peach-lib
![Generic badge](https://img.shields.io/badge/version-1.2.9-<COLOR>.svg)
JSON-RPC client library for the PeachCloud ecosystem.
`peach-lib` offers the ability to programmatically interact with the `peach-network`, `peach-oled` and `peach-stats` microservices.
## Overview
The `peach-lib` crate bundles JSON-RPC client code for making requests to the three PeachCloud microservices which expose JSON-RPC servers (`peach-network`, `peach-oled` and `peach-menu`). The full list of available RPC APIs can be found in the READMEs of the respective microservices ([peach-network](https://github.com/peachcloud/peach-network), [peach-oled](https://github.com/peachcloud/peach-oled), [peach-menu](https://github.com/peachcloud/peach-menu)), or in the [developer documentation for PeachCloud](http://docs.peachcloud.org/software/microservices/index.html).
The library also includes a custom error type, `PeachError`, which bundles the underlying error types into three variants: `JsonRpcHttp`, `JsonRpcCore` and `Serde`. When used as the returned error type in a `Result` function response, this allows convenient use of the `?` operator (as illustrated in the example usage code below).
## Usage
Define the dependency in your `Cargo.toml` file:
`peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" }`
Import the required client from the library:
```rust
use peach_lib::network_client;
```
Call one of the exposed methods:
```rust
network_client::ip("wlan0")?;
```
Further example usage can be found in the [`peach-menu`](https://github.com/peachcloud/peach-menu) code (see `src/states.rs`).
## Licensing
AGPL-3.0

18
tilde-client/src/error.rs Normal file
View File

@ -0,0 +1,18 @@
#![warn(missing_docs)]
use std::error::Error;
use std::fmt;
/// all tilde client errors
#[derive(Debug)]
pub struct TildeError {
pub(crate) message: String,
}
impl fmt::Display for TildeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for TildeError {}

73
tilde-client/src/lib.rs Normal file
View File

@ -0,0 +1,73 @@
// methods for interacting with tilde sbot
use crate::error::TildeError;
use serde_json::Value;
use std::process::{Command, exit};
mod error;
pub struct TildeClient {
name: String,
port: String
}
pub fn init_sbot() {
println!("++ init sbot!");
}
pub fn get_sbot_client() -> TildeClient {
TildeClient {
name: "name".to_string(),
port: "8009".to_string()
}
}
impl TildeClient {
pub fn run_tilde_command(&self, command: &str) -> Result<String, TildeError> {
let output = Command::new("out/release/tildefriends.standalone")
.arg(command)
.output().map_err(|e| TildeError {
message: format!("Command execution failed: {}", e),
})?;
if !output.status.success() {
return Err(TildeError { message: format!("Command failed with status: {}", output.status) })
}
let result = String::from_utf8_lossy(&output.stdout).to_string();
println!("Command output: {}", result);
Ok(result)
}
pub async fn latest_description(&self, key: &str) -> Result<String, TildeError> {
todo!();
}
pub async fn whoami(&self) -> Result<String, TildeError> {
self.run_tilde_command("get_identity")
}
pub async fn is_following(&self, from_id: &str, to_id: &str) -> Result<bool, TildeError> {
todo!();
}
pub async fn create_block(&self, key: &str) -> Result<bool, TildeError> {
todo!();
}
pub async fn get_blocks(&self, key: &str) -> Result<Vec<String>, TildeError> {
todo!();
}
pub async fn get_follows(&self, key: &str) -> Result<Vec<String>, TildeError> {
todo!();
}
pub async fn get_friends(&self, key: &str) -> Result<Vec<String>, TildeError> {
todo!();
}
pub async fn publish(&self, post: Value) -> Result<Vec<String>, TildeError> {
todo!();
}
}