62 Commits

Author SHA1 Message Date
bc190051db bundle change-password cli with peach-web 2025-05-26 17:56:04 -04:00
eaca10954e working 2025-05-23 14:01:38 -04:00
8501296ea1 working version with yunohost 2025-05-21 18:07:38 -04:00
1439bb942f working version with binary wrapper 2025-05-21 17:28:55 -04:00
ce861c69ba README 2025-05-08 15:36:39 -04:00
ac875097b3 working bash script 2025-05-08 14:51:07 -04:00
0e655841f5 almost working with tilde 2025-05-08 14:01:58 -04:00
bf05149d93 peachpub mostly working 2025-05-06 14:03:06 -04:00
fc2ea78876 mostly working with tilde 2025-03-08 16:24:26 -05:00
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
82 changed files with 3757 additions and 1390 deletions

830
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,74 @@
# [PeachCloud](http://peachcloud.org) :peach: :cloud:
_Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware product._
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
[![Build Status](https://build.coopcloud.tech/api/badges/PeachCloud/peach-workspace/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/PeachCloud/peach-workspace)
## Background
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
- November 2018 project pivot: [`%9NCyTf+oBxG0APlXRCKtrGZj3t+i+Kp3pKPN1gtFX2c=.sha256`](https://viewer.scuttlebot.io/%259NCyTf%2BoBxG0APlXRCKtrGZj3t%2Bi%2BKp3pKPN1gtFX2c%3D.sha256)
## Active Repositories
**Documentation**
- [peach-devdocs](https://github.com/peachcloud/peach-devdocs) - Developer documentation for PeachCloud in the form of a Markdown book
**Devops**
- [peach-vps](https://github.com/peachcloud/peach-vps) - Setup scripts and configuration files for deploying a PeachCloud development server
**Image building & device configuration**
- [peach-config](https://github.com/peachcloud/peach-config) - Configuration instructions, files and scripts
- [peach-img-builder](https://github.com/peachcloud/peach-img-builder) - Vmdb2 script for building a Debian disc image for Raspberry Pi with PeachCloud pre-installed
**Microservices**
- [peach-buttons](https://github.com/peachcloud/peach-buttons) - Emit GPIO events using JSON-RPC pubsub over WS
- [peach-oled](https://github.com/peachcloud/peach-oled) - Write and draw to OLED display using JSON-RPC over HTTP
- [peach-menu](https://github.com/peachcloud/peach-menu) - A menu for monitoring and interacting with the PeachCloud device
- [peach-network](https://github.com/peachcloud/peach-network) - Query and configure network interfaces using JSON-RPC over HTTP
- [peach-stats](https://github.com/peachcloud/peach-stats) - Query system statistics using JSON-RPC over HTTP
- [peach-lib](https://github.com/peachcloud/peach-lib) - JSON-RPC client library for the PeachCloud ecosystem
- [peach-monitor](https://github.com/peachcloud/peach-monitor) - Monitor network data usage and set alert flags based on user-defined thresholds
**Diagnostics**
- [peach-probe](https://github.com/peachcloud/peach-probe) - Probe PeachCloud microservices to evaluate their state and ensure correct API responses
**Web interface**
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
## Continuous Integration
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
```
git commit -m "update readme [CI SKIP]"
git push origin main
```
## Developer Diaries
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
- 1: [`%bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94=.sha256`](https://viewer.scuttlebot.io/%25bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94%3D.sha256)
- 2: [`%2L7gYAh2ih+7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM=.sha256`](https://viewer.scuttlebot.io/%252L7gYAh2ih%2B7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM%3D.sha256)
- [@mycognosist](https://github.com/mycognosist): `@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519`
- [`%mKUByRp4Gib6fqP1q2/dHg+ueSoR+Sj2Y0D7T0Np0D4=.sha256`](https://viewer.scuttlebot.io/%25mKUByRp4Gib6fqP1q2%2FdHg%2BueSoR%2BSj2Y0D7T0Np0D4%3D.sha256)
## Accounts
- [GitHub](https://github.com/peachcloud)
- [Twitter](https://twitter.com/peachcloudorg)
- [Email](mailto:peachcloudorg@gmail.com)
- [OpenCollective](https://opencollective.com/peachcloud)

View File

@ -2,73 +2,35 @@
_Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware product._
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
![image of peachcloud device](https://peach.commoninternet.net/assets/peachcloud.jpg)
## About
This project is a hack that combines PeachCloud with TildFriends.
The original PeachCloud project was paused when most development in the Scuttlebutt ecosystem stopped (reference),
but even after most funding and development and left the ecosystem, a version of the Sbot in C called TildeFriends was finished,
and continues to be maintained.
This fork of the PeachCloud project makes use of the TildeFriends sbot to make a minimal and functional PeachCloud Scuttlebutt pub,
that can be easily deployed and operated by a non-technical user.
Due to the timing and conditions of the Scuttlebutt ecosystem, and the rise of new protocols,
for this particular hack, the focus was on getting the project working in a minimal form (aka more hacky),
with less of a focus on long-term sustainability of the project as an ecosystem.
This project serves to provide a working simple-to-use pub for a P2P protocol which is in some ways frozen in amber,
as well as to share the PeachCloud interface and project, as a design-inspiration for other projects.
## Setup
TODO: how to install with yunohost
TODO: how to install without yunohost
[![Build Status](https://build.coopcloud.tech/api/badges/PeachCloud/peach-workspace/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/PeachCloud/peach-workspace)
## Background
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
- November 2018 project pivot: [`%9NCyTf+oBxG0APlXRCKtrGZj3t+i+Kp3pKPN1gtFX2c=.sha256`](https://viewer.scuttlebot.io/%259NCyTf%2BoBxG0APlXRCKtrGZj3t%2Bi%2BKp3pKPN1gtFX2c%3D.sha256)
## Active Repositories
**Documentation**
- [peach-devdocs](https://github.com/peachcloud/peach-devdocs) - Developer documentation for PeachCloud in the form of a Markdown book
**Devops**
- [peach-vps](https://github.com/peachcloud/peach-vps) - Setup scripts and configuration files for deploying a PeachCloud development server
**Image building & device configuration**
- [peach-config](https://github.com/peachcloud/peach-config) - Configuration instructions, files and scripts
- [peach-img-builder](https://github.com/peachcloud/peach-img-builder) - Vmdb2 script for building a Debian disc image for Raspberry Pi with PeachCloud pre-installed
**Microservices**
- [peach-buttons](https://github.com/peachcloud/peach-buttons) - Emit GPIO events using JSON-RPC pubsub over WS
- [peach-oled](https://github.com/peachcloud/peach-oled) - Write and draw to OLED display using JSON-RPC over HTTP
- [peach-menu](https://github.com/peachcloud/peach-menu) - A menu for monitoring and interacting with the PeachCloud device
- [peach-network](https://github.com/peachcloud/peach-network) - Query and configure network interfaces using JSON-RPC over HTTP
- [peach-stats](https://github.com/peachcloud/peach-stats) - Query system statistics using JSON-RPC over HTTP
- [peach-lib](https://github.com/peachcloud/peach-lib) - JSON-RPC client library for the PeachCloud ecosystem
- [peach-monitor](https://github.com/peachcloud/peach-monitor) - Monitor network data usage and set alert flags based on user-defined thresholds
**Diagnostics**
- [peach-probe](https://github.com/peachcloud/peach-probe) - Probe PeachCloud microservices to evaluate their state and ensure correct API responses
**Web interface**
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
## Continuous Integration
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
```
git commit -m "update readme [CI SKIP]"
git push origin main
```
## Developer Diaries
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
- 1: [`%bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94=.sha256`](https://viewer.scuttlebot.io/%25bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94%3D.sha256)
- 2: [`%2L7gYAh2ih+7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM=.sha256`](https://viewer.scuttlebot.io/%252L7gYAh2ih%2B7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM%3D.sha256)
- [@mycognosist](https://github.com/mycognosist): `@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519`
- [`%mKUByRp4Gib6fqP1q2/dHg+ueSoR+Sj2Y0D7T0Np0D4=.sha256`](https://viewer.scuttlebot.io/%25mKUByRp4Gib6fqP1q2%2FdHg%2BueSoR%2BSj2Y0D7T0Np0D4%3D.sha256)
## Accounts
- [GitHub](https://github.com/peachcloud)
- [Twitter](https://twitter.com/peachcloudorg)
- [Email](mailto:peachcloudorg@gmail.com)
- [OpenCollective](https://opencollective.com/peachcloud)
- [Original PeachCloud ReadMe](/ORIGINAL-PEACHCLOUD-README.md)
- [Original PeachCloud Documentation](https://peach.commoninternet.net)

4
pdeploy.sh Executable file
View File

@ -0,0 +1,4 @@
#! /bin/bash
cargo build --package peach-web --release
rsync -azvh /home/notplants/computer/projects/peachpub/peach-workspace/target/release/peach-web root@159.89.42.156:/var/www/peachpub_ynh/peach-web
ssh root@159.89.42.156 'systemctl restart peachpub_ynh-peach-web'

View File

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

View File

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

View File

@ -10,6 +10,7 @@ mod setup_peach_deb;
mod status;
mod update;
mod utils;
mod wait_for_sbot;
use clap::arg_enum;
use log::error;
@ -61,6 +62,10 @@ enum PeachConfig {
/// It takes an address argument of the form host:port
#[structopt(name = "publish-address")]
PublishAddress(PublishAddressOpts),
/// Wait for a successful connection to sbot
#[structopt(name = "wait-for-sbot")]
WaitForSbot,
}
#[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

@ -9,10 +9,12 @@ async-std = "1.10"
chrono = "0.4"
dirs = "4.0"
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-http = "0.5"
jsonrpc-core = "8.0"
jsonrpc_client = "0.7"
log = "0.4"
nanorand = { version = "0.6", features = ["getrandom"] }
regex = "1"
@ -22,3 +24,4 @@ serde_yaml = "0.8"
toml = "0.5"
sha3 = "0.10"
lazy_static = "1.4"
anyhow = "1.0.86"

View File

@ -59,8 +59,11 @@ pub fn get_peach_config_defaults() -> HashMap<String, String> {
("SSB_ADMIN_IDS", ""),
("ADMIN_PASSWORD_HASH", "47"),
("TEMPORARY_PASSWORD_HASH", ""),
("GO_SBOT_DATADIR", "/home/peach/.ssb-go"),
("GO_SBOT_SERVICE", "go-sbot.service"),
("TILDE_SBOT_DATADIR", "/home/peach/.local/share/tildefriends/"),
("TILDE_SBOT_SERVICE", "tilde-sbot.service"),
("TILDE_BINARY_PATH", "/home/peach/tildefriends.standalone"),
("TILDE_WRAPPER_PATH", "/home/peach/run-tilde-sbot.sh"),
("TILDE_CONFIG_PATH", "/home/peach/.local/share/tildefriends/tilde-sbot.toml"),
("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
("PEACH_HOMEDIR", "/home/peach"),
("PEACH_WEBDIR", "/usr/share/peach-web"),
@ -285,8 +288,12 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
// looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String>
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> {
let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?;
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
Ok(ssb_admin_ids)
if ssb_admin_ids_str.trim().is_empty() {
Ok(vec![])
} else {
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
Ok(ssb_admin_ids)
}
}
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec

View File

@ -1,9 +1,9 @@
#![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.
use golgi::GolgiError;
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.
#[derive(Debug)]
@ -104,8 +104,18 @@ pub enum PeachError {
path: String,
},
/// Represents a Golgi error
Golgi(GolgiError),
/// Represents a JsonRpcError with Solar
JsonRpcError(JsonRpcError),
/// Represents an Anyhow error with Solar
SolarClientError(String),
/// Represents an Anyhow error with Solar
TildeClientError(String),
/// Represents an error with encoding or decoding an SsbMessage
SsbMessageError(String),
}
impl std::error::Error for PeachError {
@ -134,7 +144,10 @@ impl std::error::Error for PeachError {
PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source),
PeachError::Golgi(_) => None,
PeachError::JsonRpcError(_) => None,
PeachError::SolarClientError(_) => None,
PeachError::TildeClientError(_) => None,
PeachError::SsbMessageError(_) => None,
}
}
}
@ -192,7 +205,10 @@ impl std::fmt::Display for PeachError {
PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path)
}
PeachError::Golgi(ref err) => err.fmt(f),
PeachError::JsonRpcError(ref err) => err.fmt(f),
PeachError::SolarClientError(ref err) => err.fmt(f),
PeachError::TildeClientError(ref err) => err.fmt(f),
PeachError::SsbMessageError(ref err) => err.fmt(f),
}
}
}
@ -263,8 +279,15 @@ impl From<string::FromUtf8Error> for PeachError {
}
}
impl From<GolgiError> for PeachError {
fn from(err: GolgiError) -> PeachError {
PeachError::Golgi(err)
impl From<JsonRpcError> for PeachError {
fn from(err: JsonRpcError) -> PeachError {
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 sbot;
pub mod stats_client;
pub mod ssb_messages;
// re-export error types
pub use jsonrpc_client_core;
pub use jsonrpc_core;
pub use serde_json;
pub use serde_yaml;
pub use tilde_client;

View File

@ -1,9 +1,9 @@
use async_std::task;
use golgi::{sbot::Keystore, Sbot};
use log::debug;
use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crate::sbot::init_sbot;
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct,
@ -108,36 +108,20 @@ using this link: http://peach.local/auth/reset",
// use golgi to send a private message on scuttlebutt
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
Ok(_) => (),
Err(e) => return Err(PeachError::Sbot(e)),
Err(e) => return Err(e),
}
}
Ok(())
}
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
let msg = msg.to_string();
let recipient = vec![recipient.to_string()];
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), PeachError> {
// initialise sbot connection with ip:port and shscap from config file
let mut sbot_client = match sbot_config {
// 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())?,
};
let mut sbot_client = init_sbot().await?;
debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.publish_private(msg, recipient).await {
match sbot_client.private_message(recipient, msg).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to publish private message: {}", e)),
Err(e) => Err(PeachError::TildeClientError(format!("Failed to publish private message: {}", e))),
}
}

View File

@ -1,8 +1,8 @@
//! 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 golgi::{sbot::Keystore, Sbot};
use std::os::linux::raw::ino_t;
use tilde_client::{TildeClient};
use log::debug;
use crate::config_manager;
@ -30,7 +30,7 @@ fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
/* SBOT-RELATED TYPES AND METHODS */
/// go-sbot process status.
/// solar-sbot process status.
#[derive(Debug, Serialize, Deserialize)]
pub struct SbotStatus {
/// Current process state.
@ -62,15 +62,16 @@ impl Default for 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> {
let mut status = SbotStatus::default();
// note this command does not need to be run as sudo
// 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")
.arg("show")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.arg(service_name)
.arg("--no-page")
.output()?;
@ -92,7 +93,7 @@ impl SbotStatus {
// because non-privileged users are able to run systemctl status
let status_output = Command::new("systemctl")
.arg("status")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?;
let service_status = str::from_utf8(&status_output.stdout)?;
@ -100,7 +101,7 @@ impl SbotStatus {
for line in service_status.lines() {
// 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)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
@ -130,10 +131,15 @@ impl SbotStatus {
}
}
// TOOD restore this
// get path to blobstore
// let blobstore_path = format!(
// "{}/blobs/sha256",
// config_manager::get_config_value("TILDE_SBOT_DATADIR")?
// );
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
@ -143,27 +149,23 @@ impl SbotStatus {
}
}
/// go-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize)]
/// solar-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SbotConfig {
pub struct Config {
// TODO: maybe define as a Path type?
/// Directory path for the log and indexes.
pub repo: String,
/// Directory path for writing debug output.
pub debugdir: String,
pub database_directory: String,
/// Secret-handshake app-key (aka. network key).
pub shscap: String,
/// HMAC hash used to sign messages.
pub hmac: String,
/// Replication hops (1: friends, 2: friends of friends).
pub hops: u8,
pub replication_hops: u8,
/// Ip address of pub
pub ip: String,
/// Address to listen on.
pub lis: String,
/// Address to listen on for WebSocket connections.
pub wslis: String,
/// Address to for metrics and pprof HTTP server.
pub debuglis: String,
pub ssb_port: String,
/// Enable sending local UDP broadcasts.
pub localadv: bool,
/// Enable listening for UDP broadcasts and connecting.
@ -176,40 +178,54 @@ pub struct SbotConfig {
pub promisc: bool,
/// Disable the UNIX socket RPC interface.
pub nounixsock: bool,
/// Attempt to repair the filesystem before starting.
pub repair: bool,
}
/// Default configuration values for go-sbot.
// TODO: make this real
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SbotConfig {
pub database_directory: String,
pub shscap: String,
pub hmac: String,
pub replication_hops: i8,
pub ip: String,
pub ssb_port: String,
// TODO: below settings have not been configured with tilde
pub localadv: bool,
pub localdiscov: bool,
pub enable_ebt: bool,
pub promisc: bool,
pub nounixsock: bool,
}
/// Default configuration values for solar-sbot.
impl Default for SbotConfig {
fn default() -> Self {
Self {
repo: ".ssb-go".to_string(),
debugdir: "".to_string(),
database_directory: "~/.local/share/tildefriends".to_string(),
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
hmac: "".to_string(),
hops: 1,
lis: ":8008".to_string(),
wslis: ":8989".to_string(),
debuglis: "localhost:6078".to_string(),
replication_hops: 1,
ip: "".to_string(),
ssb_port: "8008".to_string(),
localadv: false,
localdiscov: false,
enable_ebt: false,
promisc: false,
nounixsock: false,
repair: false,
}
}
}
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> {
// determine path of user's go-sbot config.toml
// determine path of user's solar-sbot config.toml
let config_path = format!(
"{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")?
"{}/tilde-sbot.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
println!("TILDE_SBOT_CONFIG_PATH: {}", config_path);
let config_contents = fs::read_to_string(config_path)?;
@ -218,17 +234,17 @@ impl SbotConfig {
Ok(config)
}
/// Write the given `SbotConfig` to the go-sbot `config.toml` file.
/// Write the given `SbotConfig` to the tilde-sbot `tilde-sbot.toml` file.
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 tilde-sbot configuration, please visit tilde friends documentation\n".to_string();
// convert the provided `SbotConfig` instance to a string
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!(
"{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")?
"{}/tilde-sbot.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
// open config file for writing
@ -245,23 +261,21 @@ impl SbotConfig {
}
/// Initialise an sbot client
pub async fn init_sbot() -> Result<Sbot, PeachError> {
// read sbot config from config.toml
let sbot_config = SbotConfig::read().ok();
pub async fn init_sbot() -> Result<TildeClient, PeachError> {
// read sbot config
let sbot_config = match SbotConfig::read() {
Ok(config) => config,
// build default config if an error is returned from the read attempt
Err(_) => SbotConfig::default(),
};
debug!("Initialising an sbot client with configuration parameters");
// initialise sbot connection with ip:port and shscap from config file
let key_path = format!(
"{}/secret",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
let sbot_client = match sbot_config {
// 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?,
};
let database_path = format!("{}/db.sqlite", config_manager::get_config_value("TILDE_SBOT_DATADIR")?);
let sbot_client = TildeClient {
ssb_port: sbot_config.ssb_port,
tilde_binary_path: config_manager::get_config_value("TILDE_BINARY_PATH")?,
tilde_database_path: database_path,
};
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]
name = "peach-network"
version = "0.4.2"
version = "0.5.0"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021"
description = "Query and configure network interfaces."

View File

@ -14,6 +14,7 @@
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
use std::{
collections::HashMap,
fs::OpenOptions,
io::prelude::*,
process::{Command, Stdio},
@ -106,8 +107,86 @@ pub struct Traffic {
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 */
/// 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
/// interface.
///

View File

@ -1,6 +1,6 @@
[package]
name = "peach-web"
version = "0.6.19"
version = "0.6.21"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
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."
@ -33,20 +33,23 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
async-std = "1.10"
async-std = { version = "1", features=["attributes", "tokio1"] }
base64 = "0.13"
chrono = "0.4"
dirs = "4.0"
env_logger = "0.8"
futures = "0.3"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
lazy_static = "1.4"
log = "0.4"
maud = "0.23"
peach-lib = { path = "../peach-lib" }
# these will be reintroduced when the full peachcloud mode is added
#peach-network = { path = "../peach-network" }
#peach-stats = { path = "../peach-stats" }
peach-network = { path = "../peach-network" }
peach-stats = { path = "../peach-stats" }
rouille = { version = "3.5", default-features = false }
temporary = "0.6"
vnstat_parse = "0.1.0"
xdg = "2.2"
jsonrpc_client = { version = "0.7", features = ["macros", "reqwest"] }
reqwest = "0.11.24"
urlencoding = "2.1.3"
rpassword = "5.0"

View File

@ -0,0 +1,34 @@
use peach_lib::password_utils::set_new_password;
use crate::error::PeachWebError;
/// Utility function to set the admin password for peach-web from the command-line.
pub fn set_peach_web_password(password: Option<String>) -> Result<(), PeachWebError> {
match password {
// read password from CLI arg
Some(password) => {
set_new_password(&password)
.map_err(|err| PeachWebError::PeachLib { source: err, msg: "Error setting password via cli".to_string() })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
// read password from tty
None => {
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
if pass1 != pass2 {
Err(PeachWebError::InvalidPassword)
} else {
set_new_password(&pass1)
.map_err(|err| PeachWebError::PeachLib { source: err, msg: "Passwords did not match".to_string() })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
}
}
}

1
peach-web/src/cli/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod change_password;

View File

@ -2,36 +2,40 @@
use std::io::Error as IoError;
use golgi::GolgiError;
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use serde_json::error::Error as JsonError;
use serde_yaml::Error as YamlError;
use peach_lib::tilde_client::TildeError;
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
FailedToRegisterDynDomain(String),
Golgi(GolgiError),
HomeDir,
Io(IoError),
Json(JsonError),
OsString,
PeachLib { source: PeachError, msg: String },
Yaml(YamlError),
Tilde(TildeError),
InvalidPassword,
NotYetImplemented,
}
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::Golgi(ref source) => Some(source),
PeachWebError::HomeDir => None,
PeachWebError::InvalidPassword => None,
PeachWebError::Io(ref source) => Some(source),
PeachWebError::Json(ref source) => Some(source),
PeachWebError::OsString => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::Tilde(ref source) => Some(source),
PeachWebError::NotYetImplemented => None
}
}
}
@ -42,11 +46,14 @@ impl std::fmt::Display for PeachWebError {
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
PeachWebError::HomeDir => write!(
f,
"Filesystem error: failed to determine home directory path"
),
PeachWebError::InvalidPassword => write!(
f,
"Failed to change password via CLI"
),
PeachWebError::Io(ref source) => write!(f, "IO error: {}", source),
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::OsString => write!(
@ -55,16 +62,12 @@ impl std::fmt::Display for PeachWebError {
),
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::Tilde(ref source) => write!(f, "Tilde 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 {
fn from(err: IoError) -> PeachWebError {
PeachWebError::Io(err)
@ -91,3 +94,9 @@ impl From<YamlError> for PeachWebError {
PeachWebError::Yaml(err)
}
}
impl From<TildeError> for PeachWebError {
fn from(err: TildeError) -> PeachWebError {
PeachWebError::Tilde(err)
}
}

View File

@ -19,11 +19,9 @@ mod public_router;
mod routes;
mod templates;
pub mod utils;
mod cli;
use std::{
collections::HashMap,
sync::{Mutex, RwLock},
};
use std::{collections::HashMap, env, sync::{Mutex, RwLock}};
use lazy_static::lazy_static;
use log::info;
@ -39,6 +37,12 @@ lazy_static! {
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.
#[derive(Debug, Clone)]
pub struct SessionData {
@ -46,9 +50,7 @@ pub struct SessionData {
}
/// Launch the peach-web server.
fn main() {
// initialize logger
env_logger::init();
fn run_webserver() {
// set ip address / hostname and port for the webserver
// defaults to "127.0.0.1:8000"
@ -115,3 +117,22 @@ fn main() {
})
});
}
/// cli entry point
fn main() {
env_logger::init();
let mut args = env::args();
let _program = args.next(); // skip program name
match args.next().as_deref() {
Some("run") => run_webserver(),
Some("change-password") => {
cli::change_password::set_peach_web_password(args.next());
}
_ => {
eprintln!("Usage: peach-web <run|change-password>");
std::process::exit(1);
}
}
}

View File

@ -166,6 +166,18 @@ pub fn mount_peachpub_routes(
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) => {
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
.reset_flash()
@ -200,14 +212,66 @@ pub fn mount_peachpub_routes(
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}) => {
routes::settings::theme::set_theme(theme)
},
(GET) (/status) => {
Response::html(routes::status::device::build_template())
},
(GET) (/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
// the other blocks matches the request
_ => Response::html(templates::not_found::build_template()).with_status_code(404)

View File

@ -26,7 +26,7 @@ pub fn handle_route(request: &Request, session_data: &mut Option<SessionData>) -
}
// set the `.ssb-go` path in order to mount the blob fileserver
let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path");
let ssb_path = sbot::get_tilde_ssb_path().expect("define ssb-go dir path");
let blobstore = format!("{}/blobs/sha256", ssb_path);
// blobstore file server

View File

@ -41,7 +41,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};
@ -102,12 +102,12 @@ pub fn handle_form(request: &Request) -> Response {
) {
Ok(_) => (
// <cookie-name>=<cookie-value>
"flash_name=success".to_string(),
"flash_msg=New password has been saved".to_string(),
"success".to_string(),
"New password has been saved".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to save new password: {}", err),
"error".to_string(),
format!("Failed to save new password: {}", err),
),
};

View File

@ -38,7 +38,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};

View File

@ -37,7 +37,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};
@ -76,8 +76,8 @@ pub fn handle_form(request: &Request, session_data: &mut Option<SessionData>) ->
debug!("Unsuccessful login attempt");
let err_msg = format!("Invalid password: {}", err);
let (flash_name, flash_msg) = (
"flash_name=error".to_string(),
format!("flash_msg={}", err_msg),
"error".to_string(),
format!("{}", err_msg),
);
// if unsuccessful login, render /login page again

View File

@ -14,8 +14,8 @@ pub fn deauthenticate(session_data: &mut Option<SessionData>) -> Response {
*session_data = None;
let (flash_name, flash_msg) = (
"flash_name=success".to_string(),
"flash_msg=Logged out".to_string(),
"success".to_string(),
"Logged out".to_string(),
);
// set the flash cookie headers and redirect to the login page

View File

@ -41,7 +41,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};
@ -100,12 +100,12 @@ pub fn handle_form(request: &Request) -> Response {
&data.new_password2,
) {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=New password has been saved. Return home to login".to_string(),
"success".to_string(),
"New password has been saved. Return home to login".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to reset password: {}", err),
"error".to_string(),
format!("Failed to reset password: {}", err),
),
};

View File

@ -21,8 +21,8 @@ pub fn handle_form() -> Response {
Ok(_) => {
debug!("Sent temporary password to device admin(s)");
(
"flash_name=success".to_string(),
"flash_msg=A temporary password has been sent to the admin(s) of this device"
"success".to_string(),
"A temporary password has been sent to the admin(s) of this device"
.to_string(),
)
}

View File

@ -29,67 +29,67 @@ pub fn build_template() -> PreEscaped<String> {
}
}
" to start the sbot. If the server starts successfully, you will see a green smiley face on the home page. If the face is orange and sleeping, that means the sbot is still inactive (ie. the process is not running). If the face is red and dead, that means the sbot failed to start - indicated an error. For now, the best way to gain insight into the problem is to check the systemd log. Open a terminal and enter: "
code { "systemctl status go-sbot.service" }
code { "systemctl status tilde-sbot.service" }
". The log output may give some clues about the source of the error."
}
}
(PreEscaped("<!-- BUG REPORTS -->"))
details {
summary class="card-text link" { "Submit a bug report" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Bug reports can be submitted by "
strong {
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" {
"filing an issue"
}
}
" on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions."
}
}
(PreEscaped("<!-- REQUEST SUPPORT -->"))
details {
summary class="card-text link" { "Share feedback & request support" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help."
}
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Alternatively, we have a "
strong {
a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray" {
"Matrix channel"
}
}
" for discussion about PeachCloud and you can also reach out to @glyph "
strong {
a href="mailto:glyph@mycelial.technology" class="link font-gray" {
"via email"
}
}
"."
}
}
(PreEscaped("<!-- CONTRIBUTE -->"))
details {
summary class="card-text link" { "Contribute to PeachCloud" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our "
strong {
a href="https://opencollective.com/peachcloud" class="link font-gray" {
"Open Collective"
}
}
" page."
}
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the "
strong {
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray" {
"main PeachCloud git repository"
}
}
" to find out more details or contact the team via Scuttlebutt, Matrix or email."
}
}
// (PreEscaped("<!-- BUG REPORTS -->"))
// details {
// summary class="card-text link" { "Submit a bug report" }
// p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
// "Bug reports can be submitted by "
// strong {
// a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" {
// "filing an issue"
// }
// }
// " on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions."
// }
// }
// (PreEscaped("<!-- REQUEST SUPPORT -->"))
// details {
// summary class="card-text link" { "Share feedback & request support" }
// p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
// "You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help."
// }
// p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
// "Alternatively, we have a "
// strong {
// a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray" {
// "Matrix channel"
// }
// }
// " for discussion about PeachCloud and you can also reach out to @glyph "
// strong {
// a href="mailto:glyph@mycelial.technology" class="link font-gray" {
// "via email"
// }
// }
// "."
// }
// }
// (PreEscaped("<!-- CONTRIBUTE -->"))
// details {
// summary class="card-text link" { "Contribute to PeachCloud" }
// p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
// "PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our "
// strong {
// a href="https://opencollective.com/peachcloud" class="link font-gray" {
// "Open Collective"
// }
// }
// " page."
// }
// p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
// "Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the "
// strong {
// a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray" {
// "main PeachCloud git repository"
// }
// }
// " to find out more details or contact the team via Scuttlebutt, Matrix or email."
// }
// }
}
}
};

View File

@ -1,7 +1,7 @@
use maud::{html, PreEscaped};
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
/// 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.
pub fn build_template() -> PreEscaped<String> {
let (circle_color, center_circle_text, circle_border) = render_status_elements();
let status_url = render_status_url();
// render the home template html
let home_template = html! {
@ -63,7 +77,7 @@ pub fn build_template() -> PreEscaped<String> {
}
(PreEscaped("<!-- bottom-left -->"))
(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) } {
img class="icon-medium" src="/icons/heart-pulse.svg";
}

View File

@ -21,17 +21,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::block_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};

View File

@ -21,17 +21,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::follow_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};

View File

@ -1,5 +1,6 @@
use maud::{html, Markup, PreEscaped};
use peach_lib::sbot::SbotStatus;
use peach_lib::config_manager;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
@ -14,9 +15,9 @@ use crate::{
/// Render the invite form template.
fn invite_form_template(
flash_name: Option<&str>,
flash_msg: Option<&str>,
invite_code: Option<&str>,
flash_name: Option<String>,
flash_msg: Option<String>,
invite_code: Option<String>,
) -> Markup {
html! {
(PreEscaped("<!-- SCUTTLEBUTT INVITE FORM -->"))
@ -40,7 +41,7 @@ fn invite_form_template(
// avoid displaying the invite code-containing flash msg
@if name != "code" {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
(templates::flash::build_template(&name, &msg))
}
}
}
@ -51,10 +52,11 @@ fn invite_form_template(
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
println!("build template invites!");
// if flash_name is "code" then flash_msg will be an invite code
let invite_code = if flash_name == Some("code") {
flash_msg
let invite_code = if flash_name == Some("code".to_string()) {
flash_msg.clone()
} else {
None
};
@ -89,9 +91,11 @@ pub fn handle_form(request: &Request) -> Response {
}));
let (flash_name, flash_msg) = match sbot::create_invite(data.uses) {
Ok(code) => ("flash_name=code".to_string(), format!("flash_msg={}", code)),
Err(e) => ("flash_name=error".to_string(), format!("flash_msg={}", e)),
Ok(code) => ("code".to_string(), format!("{}", code)),
Err(e) => ("error".to_string(), format!("{}", e)),
};
println!("invite flash name: {}, {}", flash_name, flash_msg);
Response::redirect_303("/scuttlebutt/invites").add_flash(flash_name, flash_msg)
}

View File

@ -17,10 +17,10 @@ pub fn build_template() -> PreEscaped<String> {
div class="card-container" {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer" { "Search" }
a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List friends" { "Friends" }
a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List follows" { "Follows" }
a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List blocks" { "Blocks" }
// a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer" { "Search" }
// a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List friends" { "Friends" }
// a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List follows" { "Follows" }
// a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List blocks" { "Blocks" }
a id="invites" class="button button-primary center" href="/scuttlebutt/invites" title="Create invites" { "Invites" }
}
}

View File

@ -74,7 +74,7 @@ pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<S
// 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))
(templates::flash::build_template(&name, &msg))
}
}
}
@ -112,17 +112,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::publish_private_msg(data.text, recipients) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Private messaging is unavailable.".to_string(),
),
};

View File

@ -160,7 +160,7 @@ pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<S
// 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))
(templates::flash::build_template(&name, &msg))
}
}
}

View File

@ -81,7 +81,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
}
@ -139,17 +139,17 @@ pub fn handle_form(request: &Request) -> Response {
data.image,
) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Profile is unavailable.".to_string(),
),
};
@ -165,7 +165,7 @@ pub fn handle_form(request: &Request) -> Response {
}
Err(err) => {
let (flash_name, flash_msg) =
("flash_name=error".to_string(), format!("flash_msg={}", err));
("error".to_string(), format!("{}", err));
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
}
}

View File

@ -22,17 +22,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::publish_public_post(data.text) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Public posting is unavailable.".to_string(),
),
};

View File

@ -29,7 +29,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
}
@ -62,7 +62,7 @@ pub fn handle_form(request: &Request) -> Response {
}
Err(err) => {
let (flash_name, flash_msg) =
("flash_name=error".to_string(), format!("flash_msg={}", err));
("error".to_string(), format!("{}", err));
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
}
}

View File

@ -21,17 +21,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::unblock_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};

View File

@ -21,17 +21,17 @@ pub fn handle_form(request: &Request) -> Response {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::unfollow_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
"success".to_string(),
format!("{}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
"error".to_string(),
format!("{}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};

View File

@ -21,12 +21,12 @@ pub fn handle_form(request: &Request) -> Response {
// save submitted admin id to file
let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=Added SSB administrator".to_string(),
"success".to_string(),
"Added SSB administrator".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to add new administrator: {}", err),
"error".to_string(),
format!("Failed to add new administrator: {}", err),
),
};

View File

@ -20,8 +20,8 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// currently produces an error because we end up with Some(String)
// instead of Some(str)
Err(_err) => {
flash_name = Some("flash_name=error");
flash_msg = Some("flash_msg=Failed to read PeachCloud configuration file");
flash_name = Some("error".to_string());
flash_msg = Some("Failed to read PeachCloud configuration file".to_string());
None
}
};
@ -58,7 +58,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};

View File

@ -21,12 +21,12 @@ pub fn handle_form(request: &Request) -> Response {
let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
// <cookie-name>=<cookie-value>
"flash_name=success".to_string(),
"flash_msg=Removed SSB administrator".to_string(),
"success".to_string(),
"Removed SSB administrator".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to remove administrator: {}", err),
"error".to_string(),
format!("Failed to remove administrator: {}", err),
),
};

View File

@ -11,8 +11,10 @@ pub fn build_template() -> PreEscaped<String> {
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
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 {
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="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }

View File

@ -1,6 +1,7 @@
pub mod admin;
//pub mod dns;
pub mod menu;
//pub mod network;
pub mod network;
pub mod power;
pub mod scuttlebutt;
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!("{}", name), format!("{}", 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!("{}", name), format!("{}", 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!("{}", name), format!("{}", 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!("{}", name), format!("{}", 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!("{}", name), format!("{}", msg));
Response::redirect_303("/power").add_flash(flash_name, flash_msg)
}

View File

@ -33,16 +33,8 @@ fn read_status_and_config() -> (String, SbotConfig, String, String) {
Err(_) => SbotConfig::default(),
};
// split the listen address into ip and port
let (ip, port) = match sbot_config.lis.find(':') {
Some(index) => {
let (ip, port) = sbot_config.lis.split_at(index);
// remove the : from the port
(ip.to_string(), port.replace(':', ""))
}
// if no ':' separator is found, assume an ip has been configured (without port)
None => (sbot_config.lis.to_string(), String::new()),
};
let ip = sbot_config.ip.clone();
let port = sbot_config.ssb_port.clone();
(run_on_startup, sbot_config, ip, port)
}
@ -52,7 +44,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let (run_on_startup, sbot_config, ip, port) = read_status_and_config();
let (run_on_startup, sbot_config, ip, ssb_port) = read_status_and_config();
let menu_template = html! {
(PreEscaped("<!-- SBOT CONFIGURATION FORM -->"))
@ -62,42 +54,42 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
label for="hops" class="label-small font-gray" { "HOPS" }
div id="hops" style="display: flex; justify-content: space-evenly;" {
div {
@if sbot_config.hops == 0 {
input type="radio" id="hops_0" name="hops" value="0" checked;
@if sbot_config.replication_hops == 0 {
input type="radio" id="hops_0" name="replication_hops" value="0" checked;
} @else {
input type="radio" id="hops_0" name="hops" value="0";
input type="radio" id="hops_0" name="replication_hops" value="0";
}
label class="font-normal" for="hops_0" { "0" }
}
div {
@if sbot_config.hops == 1 {
input type="radio" id="hops_1" name="hops" value="1" checked;
@if sbot_config.replication_hops == 1 {
input type="radio" id="hops_1" name="replication_hops" value="1" checked;
} @else {
input type="radio" id="hops_1" name="hops" value="1";
input type="radio" id="hops_1" name="replication_hops" value="1";
}
label class="font-normal" for="hops_1" { "1" }
}
div {
@if sbot_config.hops == 2 {
input type="radio" id="hops_2" name="hops" value="2" checked;
@if sbot_config.replication_hops == 2 {
input type="radio" id="hops_2" name="replication_hops" value="2" checked;
} @else {
input type="radio" id="hops_2" name="hops" value="2";
input type="radio" id="hops_2" name="replication_hops" value="2";
}
label class="font-normal" for="hops_2" { "2" }
}
div {
@if sbot_config.hops == 3 {
input type="radio" id="hops_3" name="hops" value="3" checked;
@if sbot_config.replication_hops == 3 {
input type="radio" id="hops_3" name="replication_hops" value="3" checked;
} @else {
input type="radio" id="hops_3" name="hops" value="3";
input type="radio" id="hops_3" name="replication_hops" value="3";
}
label class="font-normal" for="hops_3" { "3" }
}
div {
@if sbot_config.hops == 4 {
input type="radio" id="hops_4" name="hops" value="4" checked;
@if sbot_config.replication_hops == 4 {
input type="radio" id="hops_4" name="replication_hops" value="4" checked;
} @else {
input type="radio" id="hops_4" name="hops" value="4";
input type="radio" id="hops_4" name="replication_hops" value="4";
}
label class="font-normal" for="hops_4" { "4" }
}
@ -106,11 +98,11 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
div class="center" style="display: flex; justify-content: space-between;" {
div style="display: flex; flex-direction: column; width: 60%; margin-bottom: 2rem;" title="IP address on which the sbot runs" {
label for="ip" class="label-small font-gray" { "IP ADDRESS" }
input type="text" id="ip" name="lis_ip" value=(ip);
input type="text" id="ip" name="ip" value=(ip);
}
div style="display: flex; flex-direction: column; width: 20%; margin-bottom: 2rem;" title="Port on which the sbot runs" {
label for="port" class="label-small font-gray" { "PORT" }
input type="text" id="port" name="lis_port" value=(port);
input type="text" id="port" name="ssb_port" value=(ssb_port);
}
}
div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Network key (aka 'caps key') to define the Scuttleverse in which the sbot operates in" {
@ -119,34 +111,35 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
}
div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Directory in which the sbot database is saved" {
label for="database_dir" class="label-small font-gray" { "DATABASE DIRECTORY" }
input type="text" id="database_dir" name="repo" value=(sbot_config.repo);
input type="text" id="database_dir" name="database_directory" value=(sbot_config.database_directory);
}
// TODO: re-add these checkboxes, if tilde adds them
div class="center" {
@if sbot_config.enable_ebt {
input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt" checked;
} @else {
input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt";
}
label class="font-normal" for="ebtReplication" title="Enable Epidemic Broadcast Tree (EBT) replication instead of legacy replication" {
"Enable EBT Replication"
}
br;
@if sbot_config.localadv {
input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv" checked;
} @else {
input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv";
}
label class="font-normal" for="lanBroadcast" title="Broadcast the IP and port of this sbot instance so that local peers can discovery it and attempt to connect" {
"Enable LAN Broadcasting"
}
br;
@if sbot_config.localdiscov {
input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov" checked;
} @else {
input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov";
}
label class="font-normal" for="lanDiscovery" title="Listen for the presence of local peers and attempt to connect if found" { "Enable LAN Discovery" }
br;
// @if sbot_config.enable_ebt {
// input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt" checked;
// } @else {
// input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt";
// }
// label class="font-normal" for="ebtReplication" title="Enable Epidemic Broadcast Tree (EBT) replication instead of legacy replication" {
// "Enable EBT Replication"
// }
// br;
// @if sbot_config.localadv {
// input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv" checked;
// } @else {
// input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv";
// }
// label class="font-normal" for="lanBroadcast" title="Broadcast the IP and port of this sbot instance so that local peers can discovery it and attempt to connect" {
// "Enable LAN Broadcasting"
// }
// br;
// @if sbot_config.localdiscov {
// input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov" checked;
// } @else {
// input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov";
// }
// label class="font-normal" for="lanDiscovery" title="Listen for the presence of local peers and attempt to connect if found" { "Enable LAN Discovery" }
// br;
@if run_on_startup == "enabled" {
input type="checkbox" id="startup" style="margin-bottom: 1rem;" name="startup" checked;
} @else {
@ -154,18 +147,9 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
}
label class="font-normal" for="startup" title="Run the pub automatically on system startup" { "Run pub when computer starts" }
br;
@if sbot_config.repair {
input type="checkbox" id="repair" name="repair" checked;
} @else {
input type="checkbox" id="repair" name="repair";
}
label class="font-normal" for="repair" title="Attempt to repair the filesystem when starting the pub" { "Attempt filesystem repair when pub starts" }
}
(PreEscaped("<!-- hidden input elements for all other config variables -->"))
input type="hidden" id="debugdir" name="debugdir" value=(sbot_config.debugdir);
input type="hidden" id="hmac" name="hmac" value=(sbot_config.hmac);
input type="hidden" id="wslis" name="wslis" value=(sbot_config.wslis);
input type="hidden" id="debuglis" name="debuglis" value=(sbot_config.debuglis);
input type="hidden" id="promisc" name="promisc" value=(sbot_config.promisc);
input type="hidden" id="nounixsock" name="nounixsock" value=(sbot_config.nounixsock);
(PreEscaped("<!-- BUTTONS -->"))
@ -176,7 +160,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};
@ -196,61 +180,56 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
templates::base::build_template(body, theme)
}
// use std::io::Read;
/// Parse the sbot configuration values and write to file.
pub fn handle_form(request: &Request, restart: bool) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
repo: String,
debugdir: String,
database_directory: String,
shscap: String,
hmac: String,
hops: u8,
lis_ip: String,
lis_port: String,
wslis: String,
debuglis: String,
replication_hops: i8,
ip: String,
ssb_port: String,
localadv: bool,
localdiscov: bool,
enable_ebt: bool,
promisc: bool,
nounixsock: bool,
startup: bool,
repair: bool,
}));
// concat the ip and port for listen address
let lis = format!("{}:{}", data.lis_ip, data.lis_port);
let lis = format!("{}:{}", data.ip, data.ssb_port);
// instantiate `SbotConfig` from form data
let config = SbotConfig {
lis,
hops: data.hops,
repo: data.repo,
debugdir: data.debugdir,
ip: data.ip,
ssb_port: data.ssb_port,
replication_hops: data.replication_hops,
database_directory: data.database_directory,
shscap: data.shscap,
localadv: data.localadv,
localdiscov: data.localdiscov,
hmac: data.hmac,
wslis: data.wslis,
debuglis: data.debuglis,
enable_ebt: data.enable_ebt,
promisc: data.promisc,
nounixsock: data.nounixsock,
repair: data.repair,
};
match data.startup {
true => {
debug!("Enabling go-sbot.service");
debug!("Enabling tilde-sbot.service");
if let Err(e) = sbot::systemctl_sbot_cmd("enable") {
warn!("Failed to enable go-sbot.service: {}", e)
warn!("Failed to enable tilde-sbot.service: {}", e)
}
}
false => {
debug!("Disabling go-sbot.service");
debug!("Disabling tilde-sbot.service");
if let Err(e) = sbot::systemctl_sbot_cmd("disable") {
warn!("Failed to disable go-sbot.service: {}", e)
warn!("Failed to disable tilde-sbot.service: {}", e)
}
}
};
@ -272,7 +251,7 @@ pub fn handle_form(request: &Request, restart: bool) -> Response {
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
let (flash_name, flash_msg) = (format!("{}", name), format!("{}", msg));
Response::redirect_303("/settings/scuttlebutt/configure").add_flash(flash_name, flash_msg)
}

View File

@ -15,7 +15,7 @@ pub fn write_config() -> Response {
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
let (flash_name, flash_msg) = (format!("{}", name), format!("{}", msg));
Response::redirect_303("/settings/scuttlebutt/configure").add_flash(flash_name, flash_msg)
}

View File

@ -48,7 +48,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
// 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))
(templates::flash::build_template(&name, &msg))
}
}
};

View File

@ -14,17 +14,17 @@ pub fn restart_sbot() -> Response {
// if stop was successful, try to start the process
Ok(_) => match systemctl_sbot_cmd("start") {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=Sbot process has been restarted".to_string(),
"success".to_string(),
"Sbot process has been restarted".to_string(),
),
Err(e) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to start the sbot process: {}", e),
"error".to_string(),
format!("Failed to start the sbot process: {}", e),
),
},
Err(e) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to stop the sbot process: {}", e),
"error".to_string(),
format!("Failed to stop the sbot process: {}", e),
),
};

View File

@ -12,12 +12,12 @@ pub fn start_sbot() -> Response {
info!("Starting go-sbot.service");
let (flash_name, flash_msg) = match systemctl_sbot_cmd("start") {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=Sbot process has been started".to_string(),
"success".to_string(),
"Sbot process has been started".to_string(),
),
Err(_) => (
"flash_name=error".to_string(),
"flash_msg=Failed to start the sbot process".to_string(),
"error".to_string(),
"Failed to start the sbot process".to_string(),
),
};

View File

@ -9,15 +9,15 @@ use crate::utils::{flash::FlashResponse, sbot::systemctl_sbot_cmd};
/// Redirect to the Scuttlebutt settings menu and communicate the outcome of
/// the attempt via a flash message.
pub fn stop_sbot() -> Response {
info!("Stopping go-sbot.service");
info!("Stopping tilde-sbot.service");
let (flash_name, flash_msg) = match systemctl_sbot_cmd("stop") {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=Sbot process has been stopped".to_string(),
"success".to_string(),
"Sbot process has been stopped".to_string(),
),
Err(_) => (
"flash_name=error".to_string(),
"flash_msg=Failed to stop the sbot process".to_string(),
"error".to_string(),
"Failed to stop the sbot process".to_string(),
),
};

View File

@ -1,238 +1,381 @@
use log::info;
use rocket::{
get,
request::FlashMessage,
response::{Flash, Redirect},
};
use rocket_dyn_templates::Template;
use serde::Serialize;
use std::{
io,
process::{Command, Output},
};
use std::process::Command;
use peach_lib::{
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus,
};
use maud::{html, Markup, PreEscaped};
use peach_lib::{config_manager, dyndns_client, oled_client};
use peach_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.
#[derive(Debug, Serialize)]
pub struct StatusContext {
pub back: Option<String>,
pub cpu_stat_percent: Option<CpuStatPercentages>,
pub disk_stats: Vec<DiskUsage>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub load_average: Option<LoadAverage>,
pub mem_stats: Option<MemStat>,
pub network_ping: String,
pub oled_ping: String,
pub dyndns_enabled: bool,
pub dyndns_is_online: bool,
pub config_is_valid: bool,
pub sbot_is_online: bool,
pub title: Option<String>,
pub uptime: Option<i32>,
/// Query systemd to determine the state of the networking service.
fn retrieve_networking_state() -> Option<String> {
// call: `systemctl show networking.service --no-page`
let networking_service_output = Command::new("systemctl")
.arg("show")
.arg("networking.service")
.arg("--no-page")
.output()
.ok()?;
let service_info = std::str::from_utf8(&networking_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())
}
impl StatusContext {
pub fn build() -> StatusContext {
// convert result to Option<CpuStatPercentages>, discard any error
let cpu_stat_percent = stats::cpu_stats_percent().ok();
let load_average = stats::load_average().ok();
let mem_stats = stats::mem_stats().ok();
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
// instead of: "is the network json-rpc server running?", we want to ask:
// "is the wpa_supplicant systemd service functioning correctly?"
let network_ping = match network_client::ping() {
Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(),
};
let oled_ping = match oled_client::ping() {
Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(),
};
/// Query systemd to determine the state of the sbot service.
fn retrieve_sbot_state() -> Option<String> {
// retrieve the name of the go-sbot service or set default
let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE")
.unwrap_or_else(|_| "go-sbot.service".to_string());
let uptime = match stats::uptime() {
Ok(secs) => {
let uptime_mins = secs / 60;
uptime_mins.to_string()
}
Err(_) => "Unavailable".to_string(),
};
let sbot_service_output = Command::new("systemctl")
.arg("show")
.arg(go_sbot_service)
.arg("--no-page")
.output()
.ok()?;
// parse the uptime string to a signed integer (for math)
let uptime_parsed = uptime.parse::<i32>().ok();
let service_info = std::str::from_utf8(&sbot_service_output.stdout).ok()?;
// serialize disk usage data into Vec<DiskUsage>
let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => disks,
Err(_) => Vec::new(),
};
// 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())
}
let mut disk_stats = Vec::new();
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
for disk in disk_usage_stats {
if disk.mountpoint == "/" {
disk_stats.push(disk);
fn retrieve_device_status_data() -> (Option<u64>, String) {
let uptime = stats::uptime().ok();
let oled_ping = match oled_client::ping() {
Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(),
};
(uptime, oled_ping)
}
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")
}
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("<!-- 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()) }
}
}
}
}
// dyndns_is_online & config_is_valid
let dyndns_enabled: bool;
let dyndns_is_online: bool;
let config_is_valid: bool;
let load_peach_config_result = load_peach_config();
match load_peach_config_result {
Ok(peach_config) => {
dyndns_enabled = peach_config.dyn_enabled;
config_is_valid = true;
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;
}
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 {
dyndns_is_online = false;
}
}
Err(_err) => {
dyndns_enabled = false;
dyndns_is_online = false;
config_is_valid = false;
} @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() {
Ok(disks) => disks,
Err(_) => Vec::new(),
};
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
let disk_usage = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
html! {
@if let Some(disk) = disk_usage {
// calculate free disk space in megabytes
@let disk_free_mb = disk.one_k_blocks_free / 1024;
// calculate free disk space in gigabytes
@let disk_free_gb = disk_free_mb / 1024;
// render disk free value as megabytes or gigabytes based on size
@let disk_free_value = if disk_free_mb > 1024 {
format!("{} GB", disk_free_gb)
} else {
format!("{} MB", disk_free_mb)
};
div class="flex-grid" {
span class="card-text" { "Disk" }
span class="label-small push-right" { (disk.used_percentage) "% (" (disk_free_value) " free)" }
}
meter value=(disk.used_percentage) min="0" max="100" title="Disk usage" {
div class="meter-gauge" {
span style={ "width: " (disk.used_percentage) "%;" } {
"Disk Usage"
}
}
}
} @else {
p class="card-text" { "Disk usage data unavailable" }
}
}
}
/// Display system uptime in hours and minutes.
fn render_uptime_capsule(uptime: Option<u64>) -> Markup {
html! {
@if let Some(uptime_secs) = uptime {
@let uptime_mins = uptime_secs / 60;
@if uptime_mins < 60 {
// display system uptime in minutes
p class="capsule center-text" {
"Uptime: " (uptime_mins) " minutes"
}
} @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))
}
}
// 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 {
back: None,
cpu_stat_percent,
disk_stats,
flash_name: None,
flash_msg: None,
load_average,
mem_stats,
network_ping,
oled_ping,
dyndns_enabled,
dyndns_is_online,
config_is_valid,
sbot_is_online,
title: None,
uptime: uptime_parsed,
}
}
}
#[get("/")]
pub fn device_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = StatusContext::build();
context.back = Some("/".to_string());
context.title = Some("Device Status".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("status/device", &context)
}
// HELPERS AND ROUTES FOR /power/reboot
/// 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)
let body = templates::nav::build_template(device_status_template, "Device Status", Some("/"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

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

View File

@ -1,21 +1,285 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use vnstat_parse::Vnstat;
use crate::context::network::NetworkStatusContext;
use crate::routes::authentication::Authenticated;
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
// HELPERS AND ROUTES FOR /status/network
enum NetworkState {
AccessPoint,
WiFiClient,
}
#[get("/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());
// ROUTE: /status/network
if let Some(flash) = flash {
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
/// Render the cog icon which is used as a link to the network settings page.
fn render_network_config_icon() -> Markup {
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

@ -102,7 +102,7 @@ fn memory_element(memory: Option<u32>) -> Markup {
fn hops_element() -> Markup {
// retrieve go-sbot systemd process status
let hops = match SbotConfig::read() {
Ok(conf) => conf.hops,
Ok(conf) => conf.replication_hops,
_ => 0,
};

View File

@ -14,7 +14,7 @@ pub fn build_template(
let theme = theme::get_theme();
// 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
"dark" => (
"/icons/hermies_hex_light.svg",
@ -56,8 +56,7 @@ pub fn build_template(
a class="nav-item" href="/" {
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
}
// render the pre-defined theme-switcher icon
(switcher)
(theme_switcher)
}
}
}

View File

@ -1,21 +1,21 @@
use rouille::{input, Request, Response};
use urlencoding;
/// Flash message trait for `Request`.
pub trait FlashRequest {
/// Retrieve the flash message cookie values from a `Request`.
fn retrieve_flash(&self) -> (Option<&str>, Option<&str>);
fn retrieve_flash(&self) -> (Option<String>, Option<String>);
}
impl FlashRequest for Request {
fn retrieve_flash(&self) -> (Option<&str>, Option<&str>) {
// check for flash cookies
fn retrieve_flash(&self) -> (Option<String>, Option<String>) {
let flash_name = input::cookies(self)
.find(|&(n, _)| n == "flash_name")
// return the value of the cookie (key is already known)
.map(|key_val| key_val.1);
.and_then(|(_, val)| urlencoding::decode(&val).ok().map(|s| s.into_owned()));
let flash_msg = input::cookies(self)
.find(|&(n, _)| n == "flash_msg")
.map(|key_val| key_val.1);
.and_then(|(_, val)| urlencoding::decode(&val).ok().map(|s| s.into_owned()));
(flash_name, flash_msg)
}
@ -31,9 +31,12 @@ pub trait FlashResponse {
impl FlashResponse for Response {
fn add_flash(self, flash_name: String, flash_msg: String) -> Response {
let flash_name = urlencoding::encode(&flash_name).into_owned();
let flash_msg = urlencoding::encode(&flash_msg).into_owned();
// set the flash cookie headers
self.with_additional_header("Set-Cookie", format!("{}; Max-Age=1", flash_name))
.with_additional_header("Set-Cookie", format!("{}; Max-Age=1", flash_msg))
self.with_additional_header("Set-Cookie", format!("flash_name={}; Max-Age=1;", flash_name))
.with_additional_header("Set-Cookie", format!("flash_msg={}; Max-Age=1;", flash_msg))
}
fn reset_flash(self) -> Response {

View File

@ -11,33 +11,36 @@ use std::{
use async_std::task;
use dirs;
use futures::stream::TryStreamExt;
use golgi::{
api::friends::RelationshipQuery, blobs, messages::SsbMessageKVT, sbot::Keystore, Sbot,
};
use log::debug;
use peach_lib::config_manager;
use peach_lib::sbot::SbotConfig;
use peach_lib::sbot::init_sbot;
use peach_lib::ssb_messages::SsbMessageKVT;
use rouille::input::post::BufferedFile;
use temporary::Directory;
use peach_lib::serde_json::json;
use peach_lib::tilde_client::{TildeClient, TildeError};
use crate::{error::PeachWebError, utils::sbot};
// SBOT HELPER FUNCTIONS
/// Executes a systemctl command for the go-sbot.service process.
/// Executes a systemctl command for the solar-sbot.service process.
pub fn systemctl_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
let output = Command::new("sudo")
let mut command = Command::new("sudo");
command
.arg("systemctl")
.arg(cmd)
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.output()?;
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?);
println!("systemctl command: {:?}", command);
let output = command.output()?;
println!("systemctl output: {:?}", output);
Ok(output)
}
/// Executes a systemctl stop command followed by start command.
/// Returns a redirect with a flash message stating the output of the restart attempt.
pub fn restart_sbot_process() -> (String, String) {
debug!("Restarting go-sbot.service");
debug!("Restarting solar-sbot.service");
match systemctl_sbot_cmd("stop") {
// if stop was successful, try to start the process
Ok(_) => match systemctl_sbot_cmd("start") {
@ -64,23 +67,9 @@ pub fn restart_sbot_process() -> (String, String) {
}
/// Initialise an sbot client with the given configuration parameters.
pub async fn init_sbot_with_config(
sbot_config: &Option<SbotConfig>,
) -> Result<Sbot, PeachWebError> {
pub async fn init_sbot_client() -> Result<TildeClient, PeachWebError> {
debug!("Initialising an sbot client with configuration parameters");
// initialise sbot connection with ip:port and shscap from config file
let key_path = format!(
"{}/secret",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
let sbot_client = match sbot_config {
// 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?,
};
let sbot_client = init_sbot().await?;
Ok(sbot_client)
}
@ -123,47 +112,45 @@ pub fn validate_public_key(public_key: &str) -> Result<(), String> {
/// 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
/// the total number of locally-authored messages.
pub fn latest_sequence_number() -> Result<u64, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
pub fn latest_sequence_number() -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
let mut sbot_client = init_sbot_client().await?;
let sequence_num = sbot_client.latest_sequence_number().await?;
Ok(sequence_num)
// 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 mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
// let history_stream = sbot_client.feed(&id).await?;
// there will be zero messages when the sbot is run for the first time
if msgs.is_empty() {
Ok(0)
} else {
// reverse the list of messages so we can easily reference the latest one
msgs.reverse();
// return the sequence number of the latest msg
Ok(msgs[0].value.sequence)
}
// let mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
//
// // there will be zero messages when the sbot is run for the first time
// if msgs.is_empty() {
// Ok(0)
// } else {
// // reverse the list of messages so we can easily reference the latest one
// msgs.reverse();
//
// // return the sequence number of the latest msg
// Ok(msgs[0].value.sequence)
// }
})
}
pub fn create_invite(uses: u16) -> Result<String, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
pub fn create_invite(uses: u16) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
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");
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")?;
if !domain.is_empty() {
invite_code = domain + &invite_code[4..];
}
let external_domain = config_manager::get_config_value("EXTERNAL_DOMAIN").ok();
let mut invite_code = sbot_client.create_invite(uses as i32, external_domain.as_deref()).await?;
Ok(invite_code)
})
@ -205,11 +192,9 @@ impl Profile {
/// Retrieve the profile info for the given public key.
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 {
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?;
@ -221,32 +206,11 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
// we are not dealing with the local profile
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
profile.following = match sbot_client.friends_is_following(follow_query).await {
Ok(following) if following == "true" => Some(true),
Ok(following) if following == "false" => Some(false),
_ => None,
};
profile.following = Some(sbot_client.is_following(&local_id, &peer_id).await?);
// TODO: i don't like that we have to instantiate the same query object
// twice. see if we can streamline this in golgi
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,
};
// TODO: implement this check in solar_client so that this can be a real value
profile.blocking = Some(false);
peer_id
} else {
@ -257,7 +221,7 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
};
// 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
for (key, val) in info {
match key.as_str() {
@ -267,26 +231,27 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
_ => (),
}
}
//
// assign the ssb public key
// (could be for the local profile or a peer)
profile.id = Some(id);
// determine the path to the blob defined by the value of `profile.image`
if let Some(ref blob_id) = profile.image {
profile.blob_path = match blobs::get_blob_path(blob_id) {
Ok(path) => {
// if we get the path, check if the blob is in the blobstore.
// this allows us to default to a placeholder image in the template
if let Ok(exists) = blob_is_stored_locally(&path).await {
profile.blob_exists = exists
};
Some(path)
}
Err(_) => None,
}
}
// TODO: blobs support
// // determine the path to the blob defined by the value of `profile.image`
// if let Some(ref blob_id) = profile.image {
// profile.blob_path = match blobs::get_blob_path(blob_id) {
// Ok(path) => {
// // if we get the path, check if the blob is in the blobstore.
// // this allows us to default to a placeholder image in the template
// if let Ok(exists) = blob_is_stored_locally(&path).await {
// profile.blob_exists = exists
// };
//
// Some(path)
// }
// Err(_) => None,
// }
// }
Ok(profile)
})
@ -301,16 +266,14 @@ pub fn update_profile_info(
new_name: Option<String>,
new_description: Option<String>,
image: Option<BufferedFile>,
) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
let mut sbot_client = init_sbot_client().await?;
// track whether the name, description or image have been updated
// // track whether the name, description or image have been updated
let mut name_updated: bool = false;
let mut description_updated: bool = false;
let mut image_updated: bool = false;
@ -321,9 +284,9 @@ pub fn update_profile_info(
if name != current_name {
debug!("Publishing a new Scuttlebutt profile name");
if let Err(e) = sbot_client.publish_name(&name).await {
return Err(format!("Failed to update name: {}", e));
return Err(PeachWebError::Tilde(TildeError {message: (format!("Failed to update name: {}", e))}));
} else {
name_updated = true
name_updated = true;
}
}
}
@ -333,7 +296,7 @@ pub fn update_profile_info(
if description != current_description {
debug!("Publishing a new Scuttlebutt profile description");
if let Err(e) = sbot_client.publish_description(&description).await {
return Err(format!("Failed to update description: {}", e));
return Err(PeachWebError::Tilde(TildeError {message: (format!("Failed to update description: {}", e))}));
} else {
description_updated = true
}
@ -349,12 +312,14 @@ pub fn update_profile_info(
// if the file was successfully added to the blobstore,
// publish an about image message with the blob id
if let Err(e) = sbot_client.publish_image(&blob_id).await {
return Err(format!("Failed to update image: {}", e));
return Err(PeachWebError::Tilde(TildeError {message: (format!("Failed to update image: {}", e))}));
} else {
image_updated = true
}
}
Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)),
Err(e) => {
return Err(PeachWebError::Tilde(TildeError {message: (format!("Failed to add image to blob store: {}", e))}));
}
}
} else {
image_updated = false
@ -371,53 +336,49 @@ pub fn update_profile_info(
}
/// Follow a peer.
pub fn follow_peer(public_key: &str) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
pub fn follow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
let mut sbot_client = init_sbot_client()
.await?;
debug!("Following a Scuttlebutt peer");
match sbot_client.follow(public_key).await {
Ok(_) => Ok("Followed peer".to_string()),
Err(e) => Err(format!("Failed to follow peer: {}", e)),
}
Err(PeachWebError::NotYetImplemented)
})
}
/// Unfollow a peer.
pub fn unfollow_peer(public_key: &str) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
pub fn unfollow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
let mut sbot_client = init_sbot_client()
.await?;
debug!("Unfollowing a Scuttlebutt peer");
match sbot_client.unfollow(public_key).await {
Ok(_) => Ok("Unfollowed peer".to_string()),
Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
}
Err(PeachWebError::NotYetImplemented)
// debug!("Unfollowing a Scuttlebutt peer");
// match sbot_client.unfollow(public_key).await {
// Ok(_) => Ok("Unfollowed peer".to_string()),
// Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
// }
})
}
/// Block a peer.
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();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
let mut sbot_client = init_sbot_client()
.await
.map_err(|e| e.to_string())?;
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()),
Err(e) => Err(format!("Failed to block peer: {}", e)),
}
@ -425,176 +386,161 @@ pub fn block_peer(public_key: &str) -> Result<String, String> {
}
/// Unblock a peer.
pub fn unblock_peer(public_key: &str) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
pub fn unblock_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
let mut sbot_client = init_sbot_client()
.await?;
debug!("Unblocking a Scuttlebutt peer");
match sbot_client.unblock(public_key).await {
Ok(_) => Ok("Unblocked peer".to_string()),
Err(e) => Err(format!("Failed to unblock peer: {}", e)),
}
Err(PeachWebError::NotYetImplemented)
// match sbot_client.unblock(public_key).await {
// 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.
pub fn get_blocks_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 {
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 mut peer_list = Vec::new();
let blocks = sbot_client.get_blocks(&self_id).await?;
if !blocks.is_empty() {
for peer in blocks.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
// retrieve the profile info for the given peer
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());
let peer_info = get_peer_info(&key).await?;
// push profile info to peer_list vec
peer_list.push(peer_info)
to_return.push(peer_info)
}
}
// return the list of blocked peers
Ok(peer_list)
// return the list of peers
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 tilde_profile_info = sbot_client.get_profile_info(key).await.map_err(|err| {
println!("error getting profile info: {}", err);
err
}
)?;
let mut peer_info = HashMap::new();
tilde_profile_info.get("name").and_then(|val| val.as_str()).map(|val| {
peer_info.insert("name".to_string(), val.to_string());
});
tilde_profile_info.get("description").and_then(|val| val.as_str()).map(|val| {
peer_info.insert("description".to_string(), val.to_string());
});
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), key.to_string());
// TODO: display profile photo blob
// // 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()),
// };
// }
// }
Ok(peer_info)
}
/// Retrieve a list of peers followed by the local public key.
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 {
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 mut peer_list = Vec::new();
let follows = sbot_client.get_follows(&self_id).await?;
if !follows.is_empty() {
for peer in follows.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
// retrieve the profile info for the given peer
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()),
};
}
}
let peer_info = get_peer_info(&key).await?;
// push profile info to peer_list vec
peer_list.push(peer_info)
to_return.push(peer_info)
}
}
// return the list of peers
Ok(peer_list)
Ok(to_return)
})
}
/// Retrieve a list of peers friended by the local public key.
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 {
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
let mut peer_list = Vec::new();
if !follows.is_empty() {
for peer in follows.iter() {
if !friends.is_empty() {
for peer in friends.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
let peer_id = peer.trim().replace('"', "");
// retrieve the profile info for the given peer
let mut peer_info = sbot_client.get_profile_info(&peer_id).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()),
};
}
}
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
let peer_info = get_peer_info(&key).await?;
// check if the peer follows us (making us friends)
let follow_query = RelationshipQuery {
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)
}
_ => (),
};
// push profile info to peer_list vec
to_return.push(peer_info)
}
}
// return the list of peers
Ok(peer_list)
Ok(to_return)
})
}
/// Retrieve the local public key (id).
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();
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?;
@ -604,16 +550,20 @@ pub fn get_local_id() -> Result<String, Box<dyn Error>> {
/// Publish a public post.
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();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
let mut sbot_client = init_sbot_client()
.await
.map_err(|e| e.to_string())?;
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()),
Err(e) => Err(format!("Failed to publish post: {}", e)),
}
@ -621,38 +571,33 @@ pub fn publish_public_post(text: String) -> Result<String, String> {
}
/// Publish a private message.
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
let mut sbot_client = init_sbot_client()
.await?;
debug!("Publishing a new Scuttlebutt private message");
match sbot_client
.publish_private(text.to_string(), recipients)
.await
{
Ok(_) => Ok("Published private message".to_string()),
Err(e) => Err(format!("Failed to publish private message: {}", e)),
for recipient in &recipients {
sbot_client.private_message(recipient, &text).await?;
}
Ok("Published private message".to_string())
})
}
// FILEPATH FUNCTIONS
/// Return the path of the ssb-go directory.
pub fn get_go_ssb_path() -> Result<String, PeachWebError> {
let go_ssb_path = match SbotConfig::read() {
Ok(conf) => conf.repo,
/// Return the path of the tilde-sbot directory.
pub fn get_tilde_ssb_path() -> Result<String, PeachWebError> {
let tilde_ssb_path = match SbotConfig::read() {
Ok(conf) => conf.database_directory,
// return the default path if unable to read `config.toml`
Err(_) => {
// determine the home directory
let mut home_path = dirs::home_dir().ok_or(PeachWebError::HomeDir)?;
// add the go-ssb subdirectory
home_path.push(".ssb-go");
// add the .ssb-tilde subdirectory
home_path.push(".ssb-tilde");
// convert the PathBuf to a String
home_path
.into_os_string()
@ -660,12 +605,12 @@ pub fn get_go_ssb_path() -> Result<String, PeachWebError> {
.map_err(|_| PeachWebError::OsString)?
}
};
Ok(go_ssb_path)
Ok(tilde_ssb_path)
}
/// Check whether a blob is in the blobstore.
pub async fn blob_is_stored_locally(blob_path: &str) -> Result<bool, PeachWebError> {
let go_ssb_path = get_go_ssb_path()?;
let go_ssb_path = get_tilde_ssb_path()?;
let complete_path = format!("{}/blobs/sha256/{}", go_ssb_path, blob_path);
let blob_exists_locally = Path::new(&complete_path).exists();
Ok(blob_exists_locally)
@ -686,25 +631,13 @@ pub async fn write_blob_to_store(image: BufferedFile) -> Result<String, PeachWeb
// write file to temporary path
fs::write(&temp_path, &image.data)?;
// open the file and read it into a buffer
let mut file = File::open(&temp_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
// create blob from file
let mut sbot_client = init_sbot_client().await?;
// hash the bytes representing the file
let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
// define the blobstore path and blob filename
let (blob_dir, blob_filename) = hex_hash.split_at(2);
let go_ssb_path = get_go_ssb_path()?;
let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir);
// create the blobstore sub-directory
fs::create_dir_all(&blobstore_sub_dir)?;
// copy the file to the blobstore
let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
fs::copy(temp_path, blob_path)?;
let blob_path = temp_path.to_str().ok_or("Error storing blob to disk").map_err(|e| PeachWebError::Tilde(TildeError {
message: format!("Error storing blob to disk: {}", e),
}))?;
let blob_id = sbot_client.store_blob(blob_path).await?;
Ok(blob_id)
}

43
run-tilde-sbot.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# Exit on error
set -e
# Usage check
if [ "$#" -lt 2 ]; then
echo "Usage: $0 <CONFIG_FILE> <TILDEFRIENDS_PATH>"
exit 1
fi
CONFIG_FILE="$1"
TILDEFRIENDS_PATH="$2"
# Extract network_key (if it exists)
NETWORK_KEY=$(grep -v '^\s*#' "$CONFIG_FILE" | grep -E '^\s*network_key\s*=' | sed -E 's/.*=\s*"?(.*?)"?\s*/\1/')
DATABASE_DIRECTORY=$(grep -v '^\s*#' "$CONFIG_FILE" \
| grep -E '^\s*database_directory\s*=' \
| sed -E 's/.*=\s*"([^"]*)"\s*/\1/')
# Extract all other key-value pairs except network_key
ARGS=$(grep -v '^\s*#' "$CONFIG_FILE" \
| grep -E '^\s*[a-zA-Z0-9_.-]+\s*=' \
| grep -v '^\s*network_key\s*=' \
| sed -E 's/\s*=\s*/=/' \
| tr -d '"' \
| paste -sd, -)
echo "ARGS: $ARGS"
[ -n "$NETWORK_KEY" ] && echo "NETWORK_KEY: $NETWORK_KEY"
[ -n "$DATABASE_DIRECTORY" ] && echo "DATABASE_DIRECTORY: $DATABASE_DIRECTORY"
CMD="\"$TILDEFRIENDS_PATH\" run"
[ -n "$ARGS" ] && CMD="$CMD -a \"$ARGS\""
[ -n "$NETWORK_KEY" ] && CMD="$CMD -k \"$NETWORK_KEY\""
[ -n "$DATABASE_DIRECTORY" ] && CMD="$CMD -d \"$DATABASE_DIRECTORY/db.sqlite\""
echo "Running command:"
echo "$CMD"
# Execute the command
eval $CMD

3
tdeploy.sh Executable file
View File

@ -0,0 +1,3 @@
#! /bin/bash
cargo build --package peach-web --release
rsync -azvh /home/notplants/computer/projects/peachpub/peach-workspace/target/release/peach-web root@10.243.137.235:/var/www/peachpub_ynh/peach-web

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 message: String,
}
impl fmt::Display for TildeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for TildeError {}

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

@ -0,0 +1,142 @@
// methods for interacting with tilde sbot
use std::collections::HashMap;
pub use crate::error::TildeError;
use serde_json::{json, Value};
use std::process::{Command, exit};
mod error;
pub struct TildeClient {
pub ssb_port: String,
pub tilde_binary_path: String,
pub tilde_database_path: String,
}
pub fn init_sbot() {
println!("++ init sbot!");
}
impl TildeClient {
pub fn run_tilde_command(&self, args: Vec<&str>) -> Result<String, TildeError> {
let mut command = Command::new(&self.tilde_binary_path);
let mut full_args = args.clone();
full_args.push("-d");
full_args.push(self.tilde_database_path.as_str());
command.args(full_args);
let output = command
.output().map_err(|e| TildeError {
message: format!("Command execution failed: {}", e),
})?;
if !output.status.success() {
println!("command: {:?}", command);
println!("stderr: {:?}", String::from_utf8_lossy(&output.stderr).to_string());
println!("stdout: {:?}", String::from_utf8_lossy(&output.stdout).to_string());
return Err(TildeError { message: format!("Command failed with status: {}", output.status) })
}
let result = String::from_utf8_lossy(&output.stdout).to_string();
println!("Command: {:?}", command);
println!("Command output: {}", result);
Ok(result)
}
pub async fn tilde_command_to_value(&self, args: Vec<&str>) -> Result<Value, TildeError> {
let result = self.run_tilde_command(args)?;
let value = serde_json::from_str(&result).map_err(|e| TildeError {
message: format!("Failed to parse JSON: {}", e),
})?;
Ok(value)
}
pub async fn whoami(&self) -> Result<String, TildeError> {
self.run_tilde_command(vec!["get_identity"]).map(|val| val.trim_end().to_string())
}
pub async fn get_profile_info(&self, key: &str) -> Result<Value, TildeError> {
self.tilde_command_to_value(vec!["get_profile", "-i", key]).await
}
pub async fn latest_sequence_number(&self) -> Result<String, TildeError> {
let key = self.whoami().await?;
// let num = self.run_tilde_command(vec!["get_sequence", "-i", &key])?.parse::<u64>().map_err(|e| TildeError {
// message: format!("Failed to parse u64 from sequence number: {}", e),
// })?;
let num = self.run_tilde_command(vec!["get_sequence", "-i", &key])?;
println!("NUM: {}", num);
Ok(num)
}
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<String, TildeError> {
let json_string = post.to_string();
let key = self.whoami().await?;
self.run_tilde_command(vec!["publish", "-u", ":admin", "-i", &key, "-c", &json_string])
}
pub async fn publish_name(&self, name: &str) -> Result<String, TildeError> {
let key = self.whoami().await?;
let about_post = json!({
"type": "about",
"about": key,
"name": name
});
self.publish(about_post).await
}
pub async fn publish_description(&self, description: &str) -> Result<String, TildeError> {
let key = self.whoami().await?;
let about_post = json!({
"type": "about",
"about": key,
"description": description
});
self.publish(about_post).await
}
pub async fn publish_image(&self, image_blob_id: &str) -> Result<String, TildeError> {
let key = self.whoami().await?;
let about_post = json!({
"type": "about",
"about": key,
"image": image_blob_id
});
self.publish(about_post).await
}
pub async fn store_blob(&self, blob_file_path: &str) -> Result<String, TildeError> {
self.run_tilde_command(vec!["store_blob", "-f", blob_file_path])
}
pub async fn private_message(&self, recipient_key: &str, message: &str) -> Result<String, TildeError> {
let self_key = self.whoami().await?;
self.run_tilde_command(vec!["private", "-u", ":admin", "-i", &self_key, "-r", recipient_key, "-t", message])
}
pub async fn create_invite(&self, num_uses: i32, external_domain: Option<&str>) -> Result<String, TildeError> {
let key = self.whoami().await?;
let address = external_domain.unwrap_or_else(|| "127.0.0.1");
self.run_tilde_command(vec!["create_invite", "-u", &num_uses.to_string(), "-i", &key, "-p", &self.ssb_port, "-a", address, "-e", "-1"])
}
}