440 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
6407495292 Merge pull request 'Update go-sbot systemctl commands (remove --user)' (#132) from fix_systemctl_calls into main
Reviewed-on: #132
2022-07-15 09:55:06 +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
05c1577f2a upgrade probes version to avoid precise_time error
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-15 09:00:31 +01:00
add169db07 bump patch version
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-14 08:57:34 +01:00
fcb17d6802 remove --user and add sudo for systemctl calls 2022-07-14 08:57:22 +01: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
01138eef35 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main7 2022-07-11 13:17:34 +02:00
2637b28380 Bump peach-config to v0.1.26 2022-07-11 13:17:24 +02:00
03a0a51f4d Merge pull request 'Add publish-address function to peach-config' (#130) from publish-address into main
Reviewed-on: #130
2022-07-11 10:25:09 +00:00
eddb167c4c Remove KVT to Value map and retrieve sequence number directly from KVT
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-07 15:54:19 +01:00
9704269c8a Remove Cargo.lock from .gitignore 2022-07-07 15:44:31 +01:00
466db8ceea Update sbot.rs to new history_stream api
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-07 13:42:32 +02:00
90badbfe30 Add cargo.lock to .gitignore
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-07 10:57:27 +02:00
7489916d5f Remove cargo.lock 2022-07-07 10:57:03 +02:00
7daab74b37 Fix golgi import
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-07 10:51:44 +02:00
58bf306d3b Fix golgi import
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-07 10:49:49 +02:00
bdac23092a Change changepassword to change-password
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-07 10:41:36 +02:00
f1ab2caa08 Fix golgi imports
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-06 12:39:55 +02:00
1fab4f3c43 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main7
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-06 12:34:45 +02:00
8dcd594dd7 Publish address 2022-07-06 12:34:24 +02:00
fcaa9e29c4 Merge pull request 'Add whoami command to peach-config' (#128) from who-am-i into main
Reviewed-on: #128
2022-07-05 13:10:26 +00:00
c6fc5c2992 Merge branch 'main' into who-am-i
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-05 12:52:21 +00:00
1258a3697d Cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-07-04 14:54:00 +02:00
7e94135839 Whoami
Some checks failed
continuous-integration/drone/pr Build is failing
2022-07-04 14:35:17 +02:00
f002f5cf3e Working on peach-config 2022-07-04 13:23:55 +02:00
fba1e91d8b Merge pull request 'Insert domain into invite' (#125) from insert-invite into main
Reviewed-on: #125
2022-06-30 13:00:46 +00:00
6621a09ec9 Insert domain into invite
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-29 16:23:41 -04:00
170b037248 Merge pull request 'Update configuration sections of README' (#122) from update_readme into main
Reviewed-on: #122
2022-06-28 10:37:12 +00:00
251aaf9237 merge main
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-28 09:16:09 +01:00
123ebc06cc Merge pull request 'Introduce JS for improved back button behaviour' (#121) from js_back_button into main
Reviewed-on: #121
2022-06-28 08:14:42 +00:00
9ce27d17c5 merge main
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-28 09:13:54 +01:00
2a8cf4ecfb update configuration sections
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-27 09:01:00 +01:00
d1a55e29d7 bump patch version and update lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-23 12:02:09 +01:00
4568577f81 add js to overlay back button functionality 2022-06-23 12:01:18 +01:00
ab0e27c14d Merge pull request 'Improve back button behaviour' (#120) from improved_back_button into main
Reviewed-on: #120
2022-06-23 11:00:21 +00:00
65b5f95a90 update lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-21 12:14:30 +01:00
a60d892e95 bump the patch version 2022-06-21 12:14:14 +01:00
5bd8a68ddf set, retrieve and reset back_url cookies 2022-06-21 12:13:36 +01:00
a6f52ce384 add cookie utils module for requests and responses 2022-06-21 12:13:04 +01:00
c71cc3992d Merge pull request 'Configure EBT replication setting' (#118) from enable_ebt_replication into main
Reviewed-on: #118
2022-06-16 11:04:39 +00:00
56fafc8d67 bump manifest version and add max as author
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-16 11:41:50 +01:00
414508f8ff merge changes from main 2022-06-16 11:40:35 +01:00
6ad5c620c1 Merge pull request 'URL encode SSB public keys on profile page' (#116) from fix_encoding_decoding into main
Reviewed-on: #116
2022-06-16 10:38:11 +00:00
76d5e6a355 fix probes version
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-16 11:19:04 +01:00
11e94fa421 merge changes from main
Some checks failed
continuous-integration/drone/pr Build is failing
2022-06-16 11:10:32 +01:00
216b60b86a bump version in manifest 2022-06-16 11:07:44 +01:00
a70f5e227d Merge pull request 'Fix link to Set New Password route' (#117) from fix_set_new_passwork_link into main
Reviewed-on: #117
2022-06-16 10:02:12 +00:00
cddcb8f9bd fix spacing in manifest
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-16 09:19:59 +01:00
8b33f8c174 Merge branch 'fix_set_new_passwork_link' of ssh://git.coopcloud.tech:2222/PeachCloud/peach-workspace into fix_set_new_passwork_link 2022-06-16 09:19:04 +01:00
1b43dc8b18 bump patch version 2022-06-16 09:18:38 +01:00
2d7b74d377 Merge branch 'main' into fix_set_new_passwork_link
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-16 08:14:45 +00:00
6b34864289 Merge pull request 'Change go-sbot.service name to be configurable' (#113) from go-sbot-service into main
Reviewed-on: #113
2022-06-15 12:03:29 +00:00
87ad2439b9 Fix cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-15 13:46:51 +02:00
5838faf128 Cargo fmt
Some checks failed
continuous-integration/drone/pr Build is failing
2022-06-15 13:37:36 +02:00
9c6fa00ec7 Fix cargo.lock
Some checks failed
continuous-integration/drone/pr Build is failing
2022-06-15 13:33:56 +02:00
a81b8b42cf Use main golgi branch 2022-06-15 12:48:51 +02:00
cdcff3475c Fix cargo.lock
Some checks failed
continuous-integration/drone/pr Build is failing
2022-06-15 12:47:11 +02:00
077c2a9178 Fix clippy errors 2022-06-15 12:44:34 +02:00
8b0b872d21 Reponse to CR 2022-06-15 12:36:44 +02:00
218a70b8f8 add lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-15 08:58:45 +01:00
50dcb2cf9e add checkbox to enable / disable ebt 2022-06-15 08:58:21 +01:00
e1877b5024 fix link to set new password
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-15 08:46:16 +01:00
0923c24693 update lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
2022-06-14 09:03:24 +01:00
f3d4ba9fe5 url-encode the ssb id for profile buttons 2022-06-14 09:03:08 +01:00
16e6d42f87 Cargo clippy
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-26 23:19:31 +02:00
3493e5adb9 Change sbot.rs to use configurable go-sbot service name 2022-05-26 23:18:22 +02:00
5147eed497 Change sbot.rs to use configurable go-sbot service name 2022-05-26 23:17:20 +02:00
9f6ba14123 Cargo fmt
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-25 14:50:41 +02:00
21fb29c322 Working sbot
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-25 14:49:05 +02:00
6c9e5fd3fd Merge branch 'go-sbot-service' of https://git.coopcloud.tech/PeachCloud/peach-workspace into change-paths
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-25 12:26:11 +02:00
3adb226969 Bump version number 2022-05-25 12:25:54 +02:00
92f516b161 Merge pull request 'Fix lockfile' (#114) from fix-lock into go-sbot-service
Some checks reported errors
continuous-integration/drone/pr Build was killed
Reviewed-on: #114
2022-05-20 14:33:18 +00:00
543470b949 Fix lockfile
Some checks reported errors
continuous-integration/drone/pr Build was killed
2022-05-20 16:28:55 +02:00
6434471599 Bump version numbers
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-20 13:55:21 +02:00
56c142a387 Make go-sbot.service name configurable
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-20 13:35:37 +02:00
7deaa00d6e Fix hardcoded path
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-20 13:31:06 +02:00
bf7f2c8e31 Merge pull request 'Update golgi init functions with keystore selector' (#107) from keystore_selector into main
Reviewed-on: #107
2022-05-16 13:12:00 +00:00
dc79833e2b merge wpactrl and golgi api updates
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-13 13:15:12 +02:00
039cbcdfc4 incorporate keystore change from golgi 2022-05-13 13:09:03 +02:00
4cd2692b9a fix probes dependency version 2022-05-13 13:07:37 +02:00
3e5e7e0f7c update wpactrl dependency api usage 2022-05-13 13:07:21 +02:00
b0b79fef24 fix probes:0.3.0 lockfile error
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-13 12:44:51 +02:00
98497fa5ae merge latest changes from main
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-13 12:35:37 +02:00
e6fd9a48cf Merge pull request 'Update peach-config to use configured paths' (#111) from change-paths2 into main
Reviewed-on: #111
2022-05-12 20:25:13 +00:00
8960df6635 Fix cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-12 21:58:15 +02:00
781af460ae Fix clippy warning
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-12 13:32:40 +02:00
4a08e4ed6d Merge pull request 'Use get_config_value for Rouille configurations' (#109) from get-config into main
Reviewed-on: #109
2022-05-12 11:31:18 +00:00
908d265de6 Bump version numbers
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-12 12:53:06 +02:00
8202d4af5f Update peach-config to use configured paths
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-12 12:49:47 +02:00
5ea6a86700 Fix clippy error
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-12 12:24:17 +02:00
99fd3be4ad Change RouilleConfig to ServerConfig
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-12 12:21:05 +02:00
e041e1c7f9 Change defaults to only be defined in config_manager.rs
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-12 11:55:59 +02:00
e10777a5a5 Merge pull request 'Enable nanorand featureflag for cross-compilation on mac os' (#108) from cross-compilation into main
Reviewed-on: #108
2022-05-12 09:27:00 +00:00
288941e8a3 Change Rouille to use get_config_value
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-12 11:26:23 +02:00
f96c950aa6 Enable nanorand featureflag for cross-compilation on mac os
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-11 17:06:12 +02:00
827ccbd4dc update lockfile
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-11 16:42:32 +02:00
c21e2d090c introduce keystore selector for golgi 2022-05-11 16:42:11 +02:00
bab33b602a Merge pull request 'Remove unecessary .to_string' (#106) from to-string into main
Reviewed-on: #106
2022-05-11 09:48:10 +00:00
b84e470a42 Remove unecessary .to_string
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-11 11:24:33 +02:00
97680f9010 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main5 2022-05-11 11:05:56 +02:00
ab0401e555 Change lazy_static to 1.4 2022-05-11 11:05:49 +02:00
810a97db8a Merge pull request 'Update config_manager to check from three sources of configuration' (#105) from config into main
Reviewed-on: #105
2022-05-11 09:03:57 +00:00
610d60d989 Fix typo
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-11 10:34:53 +02:00
f4c1bc1169 Fix cargo fmt in peach-web
All checks were successful
continuous-integration/drone/pr Build is passing
2022-05-10 13:25:53 +02:00
3ae182caa9 Fix clippy warnings in peach-web
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-10 13:23:39 +02:00
8a6ad4ad61 Fix example
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-10 13:08:32 +02:00
600f9c58bf Use &str instead of String in save_config_value
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-10 13:07:50 +02:00
2540a77af1 Working 3-way configuration
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-10 12:59:36 +02:00
5b86f754f4 Working on refactor to use hashmaps
Some checks failed
continuous-integration/drone/pr Build is failing
2022-05-09 15:53:03 +02:00
29804b0dce Get CONFIG_PATH from env_variable 2022-05-03 15:50:45 +02:00
e2ac5de6e4 Merge pull request 'Fix peach-config manifest' (#103) from fix-manifest into main
Reviewed-on: #103
2022-04-20 18:27:43 +00:00
d03de8cf5d Remove extraneous quote
All checks were successful
continuous-integration/drone/pr Build is passing
2022-04-20 13:55:26 -04:00
03720a7338 Remove unused import
All checks were successful
continuous-integration/drone/pr Build is passing
2022-04-20 12:19:15 -04:00
cf9c0c7eca Add doc comment 2022-04-20 12:18:31 -04:00
f764acc2df More concise transforms 2022-04-20 12:15:38 -04:00
a347e4726d Fix manifest 2022-04-20 12:07:32 -04:00
3d3006049b Merge pull request 'Change peach-web to use systemd system calls' (#102) from disc-image into main
Reviewed-on: #102
2022-04-19 18:41:22 +00:00
2adb3006fe Cargo fmt
All checks were successful
continuous-integration/drone/pr Build is passing
2022-04-19 12:57:09 -04:00
64b5929e5c Update peach-config to set file permissions correctly
Some checks failed
continuous-integration/drone/pr Build is failing
2022-04-19 12:50:29 -04:00
5629a048a1 Fix conflict 2022-04-18 16:45:28 -04:00
713c3da4cc fix formatting 2022-04-15 11:08:55 +02:00
92c7d7daa9 Merge pull request 'Drone CI build pipeline' (#101) from drone_ci_test into main
Reviewed-on: #101
2022-04-15 09:05:02 +00:00
5a95ade8b9 only run ci on pull request and add docs to readme [CI SKIP]
All checks were successful
continuous-integration/drone/pr Build is passing
2022-04-15 10:38:12 +02:00
00d33c2c69 remove arm64 pipeline [CI SKIP]
All checks were successful
continuous-integration/drone/pr Build is passing
2022-04-15 10:22:39 +02:00
126609a605 try arm64 ci only
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-15 10:16:55 +02:00
e4078bd1ba specify triggers twice
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-15 09:48:39 +02:00
315b04a63e Update permissions for peach home dir in peach-config 2022-04-14 16:29:57 -04:00
1866e289a6 Fix clippy warning in update.rs 2022-04-14 16:11:20 -04:00
bff86a490b Bump version number 2022-04-14 15:53:09 -04:00
65d5352c85 Bump version number of peach-config 2022-04-14 14:51:43 -04:00
df3b4b8858 Update peach-config to install go-sbot instead of peach-go-sbot 2022-04-14 14:51:27 -04:00
2f1535fbee Update peach-web to use systemd system service 2022-04-14 14:47:43 -04:00
b75aadd62d Working on peachcloud disc image 2022-04-11 10:31:45 -04:00
4662b15ba3 remove unnecessary to_owned
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-07 14:20:41 +02:00
abde4ce1b4 only specify triggers once
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-04-06 12:07:45 +02:00
c792aea2f6 add build step and arm64 pipeline
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-06 11:55:00 +02:00
b158fba147 separate ci pipeline into discreet steps
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 11:44:14 +02:00
da8d8f0ec3 add rustfmt and cargo check to ci pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 11:37:11 +02:00
271aa14322 add build status badge to root readme
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 11:33:52 +02:00
d31825f688 add tests to ci and fail on clippy warnings
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 11:13:31 +02:00
defb8f5f09 try to fix probes build issue again
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-06 10:55:41 +02:00
27e9a8295c update probes and lockfile to fix time build error
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 10:50:37 +02:00
9ad580b86f install clippy in pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 10:43:35 +02:00
a76ec08da6 move drone config to repo root and add triggers
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 10:36:10 +02:00
cf64cd9c76 add drone ci config with clippy for amd64 2022-04-06 10:32:12 +02:00
169149d607 Merge pull request 'Replace absolute paths with relative paths for system command calls' (#100) from fix_sys_cmd_paths into main
Reviewed-on: #100
2022-04-06 08:17:41 +00:00
f1eaa07f7b replace absolute paths with relative paths for system command calls 2022-04-06 10:13:52 +02:00
52c3e88b44 Merge pull request 'Fix Scuttlebutt Status page title' (#99) from fix_status_title into main
Reviewed-on: #99
2022-04-06 08:04:59 +00:00
e659102495 update body template comment 2022-04-06 09:59:17 +02:00
57b1a786a4 fix status page title 2022-04-06 09:57:32 +02:00
fded48908d Merge pull request 'Replace Rocket and Tera with Rouille and Maud' (#88) from rouille_maud into main
Reviewed-on: #88
2022-03-25 08:07:15 +00:00
46ded85feb resolve merge conflicts 2022-03-25 10:06:00 +02:00
f29659669c add route to handle resetting default sbot config 2022-03-25 09:39:06 +02:00
d6695b291d tweak no-peers element 2022-03-25 09:26:44 +02:00
aefa1525fb clarify missing name and description 2022-03-25 09:19:43 +02:00
367f0307b6 autofocus password input 2022-03-25 09:19:28 +02:00
b1c5c701e5 reduce specificity of peach-lib dependencies 2022-03-25 08:45:10 +02:00
2ae9cb5c48 remove ellipsis label class width 2022-03-25 08:43:32 +02:00
30aff5d7ac add clarity to empty peers list message 2022-03-25 08:43:15 +02:00
25e3a145fc fix alt attribute for logout icon 2022-03-24 13:42:02 +02:00
0bfad25d3d add follow, unfollow, block and unblock routes and sbot helper functions 2022-03-24 13:41:49 +02:00
952951515b update readme 2022-03-24 09:43:31 +02:00
c65f568e40 authenticate session if disable_auth env var is true 2022-03-24 09:43:25 +02:00
979ec4eb64 reintroduce logging statements 2022-03-24 09:27:06 +02:00
d9019d6a4b update git ignore and remove outdated code 2022-03-24 09:19:39 +02:00
07147f8a4f remove rocket config, context and tests 2022-03-24 09:13:25 +02:00
5fc0094146 remove tera templates 2022-03-24 09:11:26 +02:00
50afb61955 update dependencies and bump minor version 2022-03-24 09:09:08 +02:00
928afb35d3 update application configuration 2022-03-24 09:07:43 +02:00
1bdacf3632 add application configuration to replace Rocket.toml 2022-03-24 09:05:53 +02:00
deaedc4428 add temporary password reset routes 2022-03-24 09:05:26 +02:00
4a94f14dc5 update golgi dependency to use git path 2022-03-23 14:59:33 +02:00
b78fafe84d update dependencies 2022-03-23 14:56:44 +02:00
5d37c12913 implement authentication with separate public and private routers 2022-03-23 14:56:31 +02:00
3a05396afb mount blobstore and add theme support for all routes 2022-03-23 11:41:47 +02:00
41bd39d422 add themes, add public and private msg publishing 2022-03-23 11:41:21 +02:00
77c1ccb1c7 add form handler and helper for sbot config updates 2022-03-23 09:14:54 +02:00
7d9bc2d7cd reduce code repetition with class splices 2022-03-22 16:13:50 +02:00
b20822a644 satisfy clippy 2022-03-22 16:13:34 +02:00
65f0ac7630 update flash message class width 2022-03-22 16:12:47 +02:00
703f35d8b1 add profile_update module 2022-03-22 16:12:28 +02:00
084af1b486 add profile update template and route handler 2022-03-22 16:11:52 +02:00
3e918f66cf fix error flash value 2022-03-22 16:11:13 +02:00
98121f4922 use splices for rendering and url-encode the public key link 2022-03-21 16:43:22 +02:00
e19fa0f99d import profile module 2022-03-21 16:42:16 +02:00
3a7b499742 use splices for template rendering 2022-03-21 16:41:52 +02:00
85231a20c7 add profile template and route handler 2022-03-21 16:40:54 +02:00
602c6a90f1 use class splices to reduce code repetition 2022-03-21 15:17:16 +02:00
34b4cbff32 add search and invite templates and route handlers 2022-03-21 13:27:32 +02:00
112cfca67b add url comment 2022-03-21 13:26:32 +02:00
a379de179d add sbot error template 2022-03-21 11:17:42 +02:00
0353586705 add friends, follows and blocks route handlers and templates 2022-03-21 11:17:30 +02:00
4e8d93c388 move routes to the router 2022-03-20 17:17:17 +02:00
6db5e7c169 add routes and helpers for starting, stopping and restarting the sbot 2022-03-20 16:38:32 +02:00
60539adf41 add max-age and date for flash cookies 2022-03-20 16:37:53 +02:00
e8b9cb2cc1 remove default rouille features 2022-03-20 15:37:02 +02:00
cad3fc94c8 update theme import 2022-03-20 15:36:37 +02:00
976fac973d add flash messages to admin settings and auth routes 2022-03-20 15:36:24 +02:00
cd7c2bc230 move sbot helper functions to utils 2022-03-20 12:28:08 +02:00
40c4f8aaf2 implement flash cookies for auth change routes 2022-03-20 12:27:00 +02:00
70f7ad0dc6 add sbot utils, move theme utils and add flash message trait 2022-03-20 12:26:04 +02:00
31628a7155 remove old scuttlebutt routes file 2022-03-18 11:33:27 +02:00
3c49c067dd fix big circle background colour when sbot status is failed 2022-03-18 11:33:10 +02:00
729580729c add scuttlebutt peers menu and inactive template 2022-03-18 11:32:51 +02:00
59739cf6e5 further refinement of the sbot status page 2022-03-18 11:30:59 +02:00
7fe919d9a1 refine sbot status pattern matching 2022-03-18 11:25:53 +02:00
7cdf8c553d complete scuttlebutt status route 2022-03-17 16:30:26 +02:00
fe04195030 update lockfile and remove old auth routes file 2022-03-17 11:02:04 +02:00
8455e8089c add base templates, settings routes and auth routes 2022-03-17 11:01:36 +02:00
97206e0573 add links for submitting bug reports and feature suggestions 2022-03-14 11:17:54 +02:00
7acf6ef395 tiny wording change to bug issue template 2022-03-14 11:11:08 +02:00
3828998769 Merge pull request 'Add feature suggestion template' (#87) from bug_report_template into main
Reviewed-on: #87
2022-03-14 09:09:46 +00:00
440d6f9bd5 add feature suggestion template 2022-03-14 11:08:32 +02:00
59a6c7fdca Merge pull request 'Move issue template to repo root' (#86) from bug_report_template into main
Reviewed-on: #86
2022-03-14 08:27:19 +00:00
3a4b0ffffd move issue template to repo root 2022-03-14 10:26:23 +02:00
447f81a41c Merge pull request 'Add issue template for bug reporting' (#85) from bug_report_template into main
Reviewed-on: #85
2022-03-14 08:23:01 +00:00
fadad1c30b formatting 2022-03-14 10:21:50 +02:00
6395fb05e3 add issue template for bug reporting 2022-03-14 10:20:40 +02:00
d652f1a020 tiny guide typo and bold fixes 2022-03-14 10:12:44 +02:00
7c98cfcd5d add admin menu and config routes; start thinking about flash msgs 2022-03-14 09:17:31 +02:00
4a1d3e81c1 Merge pull request 'Fix and improve all login and password-related workflows' (#83) from fix_login_flows into main
Reviewed-on: #83
2022-03-13 09:14:06 +00:00
5a07eda910 update golgi dependency path 2022-03-13 11:12:12 +02:00
580771ebf2 update notes and add sbot settings config route to router 2022-03-13 11:09:39 +02:00
c794d398b8 add sbot settings config route 2022-03-13 11:09:00 +02:00
4d06eb167f incomplete sbot config route 2022-03-12 10:36:40 +02:00
eba15605c2 add scuttlebutt settings menu 2022-03-11 15:33:04 +02:00
07c18ea64d create settings menu route and move home route 2022-03-11 14:28:31 +02:00
ec288658f3 implement basic config 2022-03-11 14:27:40 +02:00
6b145d66f8 add basic application config and parser 2022-03-11 10:19:00 +02:00
23d6870f77 home template is working 2022-03-10 11:09:26 +02:00
b7cf3c1aab fix a test and update guide links 2022-03-09 13:04:44 +02:00
5b70353d6f update readme 2022-03-08 10:53:06 +02:00
67c727716c fix dark theme styling and button widths 2022-03-07 12:02:03 +02:00
5ab47cf742 made select css classes wider 2022-03-07 11:36:49 +02:00
b092f1e1c4 minor wording change for temporary password request 2022-03-07 11:36:24 +02:00
983aa0689c improve messaging for when sbot is inactive 2022-03-07 11:36:03 +02:00
1a8ac3f57f change help to guide and add text 2022-03-07 11:35:19 +02:00
af34829cb0 add reset password option to admin menu 2022-03-07 11:34:10 +02:00
824cbdbc0c save default peachcloud config if file does not exist 2022-03-07 11:26:52 +02:00
84656ff251 set default password 2022-03-07 11:26:13 +02:00
6cdd6dc41b remove unnecessary serde derivations, state-based remplate rendering, styling improvements 2022-03-04 10:58:50 +02:00
3572fd4e7b remove password hash logging 2022-03-04 10:56:07 +02:00
7fdf88eaa8 fix and improve all login and password-related workflows 2022-03-04 10:53:49 +02:00
10049f0bc6 Merge pull request 'Integrate golgi into peach_web' (#81) from golgi_integration into main
Reviewed-on: #81
2022-03-03 07:37:23 +00:00
486518002d add latest lockfile 2022-03-03 09:36:48 +02:00
3991af11c7 only render private if sbot is active, fix type in profile 2022-03-03 09:34:30 +02:00
59ef5960a4 reduce code repetition 2022-03-03 09:33:45 +02:00
69a8cc262e renamed context file 2022-03-03 08:55:58 +02:00
1479a65d59 rename sbot context module to match all scuttlebutt.rs files 2022-03-03 08:55:37 +02:00
ffe190148d minor css tweaks 2022-03-02 15:24:40 +02:00
b1724f6eb4 update sbot status context and add latest seq num 2022-03-02 15:24:05 +02:00
9a07ab3ac0 add query param for private msgs 2022-03-02 13:51:02 +02:00
06a55ade06 remove blob rendering for blocked peers 2022-03-02 13:35:21 +02:00
6cba477f15 add private message publishing 2022-03-02 11:57:10 +02:00
020d18731b add block list and implement (un)follow and (un)block 2022-03-02 08:58:54 +02:00
a38394054d implement peer search with key validation 2022-03-01 10:53:45 +02:00
814162ce7d update friends list handler and remove followers 2022-02-28 16:25:31 +02:00
03028a2278 add context builder for friends list 2022-02-28 16:25:06 +02:00
e10468c337 add basic peer lookup, improve follows and profile route handlers 2022-02-28 16:08:05 +02:00
436a516c3e add conditional rendering for profile and peers list 2022-02-28 16:06:52 +02:00
786e3f41d9 add utility to check blobstore for file 2022-02-28 16:05:53 +02:00
a491892bd9 assign public key id to profile context 2022-02-25 10:49:37 +02:00
02a1078ece make profile capsule border pink 2022-02-25 09:10:09 +02:00
6d2502257d add profile image uploader and blob saver 2022-02-25 09:09:41 +02:00
ebbcc35fbb add deps for base64 and temp dir creation 2022-02-25 09:09:07 +02:00
e3eb3be2e3 use local blobstore path util function 2022-02-25 09:08:48 +02:00
799d9de001 add io error variant and blob writer function 2022-02-25 09:08:21 +02:00
e05de8284d get profile image blob path and render in template 2022-02-23 11:54:34 +02:00
9013ccb3d6 determine blobstore path and mount blob file server 2022-02-23 11:54:08 +02:00
a37288225a remove decimal from memory and blobstore size 2022-02-23 10:22:52 +02:00
4665a9e6fa fix icon permissions; remove executable 2022-02-23 10:22:21 +02:00
1a3ddccbd6 add invite creation and display 2022-02-23 10:21:58 +02:00
fe1da62058 add template for profile update form 2022-02-11 10:32:59 +02:00
68c926609e routes and logic for profiles and publishing name, description and post 2022-02-11 10:32:39 +02:00
17ea3e7f44 add golgi dependency and error variant 2022-02-11 10:31:31 +02:00
a174027ff5 add context builders for sbot 2022-02-11 10:31:07 +02:00
f459fe47d1 add display for warning flash msgs 2022-02-11 10:30:27 +02:00
4709ec77f9 Merge pull request 'Introduce theme support (plus blobstore size)' (#80) from theme_support into main
Reviewed-on: #80
2022-02-07 07:56:30 +00:00
4e6bb15a23 update templates to use theme variables and classes 2022-02-03 16:31:17 +02:00
62191e5509 theme icons and updated hermies icons 2022-02-03 16:30:26 +02:00
da976ff4fe introduce theme attributes and variables 2022-02-03 16:29:53 +02:00
0737c435a8 add theme setter and getter, update route handlers 2022-02-03 16:29:20 +02:00
435e819648 add blobstore size lookup for sbot status 2022-02-03 16:27:20 +02:00
8f49fa55ad Merge pull request 'Add configuration reading and writing for go-sbot' (#79) from sbot_config into main
Reviewed-on: #79
2022-02-03 08:09:03 +00:00
f6292407d0 add save and restart option for config form 2022-02-02 16:32:10 +02:00
dfc173d941 add clarity about need for restart after config update 2022-02-02 16:20:30 +02:00
a46b58b206 add repair fs config option 2022-02-02 14:49:36 +02:00
33604ac0dc change form width for better mobile styling 2022-02-02 14:49:14 +02:00
f0d972f46b add repair parameter to SbotConfig 2022-02-02 14:48:39 +02:00
89b502be25 add configuration routes for the sbot 2022-02-02 14:14:12 +02:00
90a90096f4 remove option type wrappers and implement defaults for SbotConfig 2022-02-02 14:13:02 +02:00
46926bf468 add config writer method and required error variants 2022-02-01 16:07:19 +02:00
d801c957bd introduce sbot status data 2022-02-01 10:06:45 +02:00
3397e5eb75 add config file reader for go-sbot 2022-02-01 10:03:46 +02:00
e474ea519f Merge pull request 'Start and stop go-sbot process' (#78) from sbot_process_start_stop into main
Reviewed-on: #78
2022-01-27 09:24:58 +00:00
f715644e25 display system startup mode 2022-01-27 11:01:12 +02:00
c7cc310a32 add form element to enable / disable running go-sbot on startup 2022-01-27 10:57:21 +02:00
4470f949bd add ability to stop, start and restart go-sbot process 2022-01-27 10:56:39 +02:00
8e5c29ca6d add sbot stats to home template and set inner circle colour accordingly 2022-01-27 10:55:44 +02:00
00554706cb add go-sbot enabled / disabled check 2022-01-27 10:55:04 +02:00
6d9ced5ebc Merge pull request 'Improve Scuttlebutt status page' (#77) from add_sbot_status into main
Reviewed-on: #77
2022-01-26 10:10:49 +00:00
abda4373ae conditional font colour rendering 2022-01-26 12:10:28 +02:00
e718889485 update sbot status page and add live data retrieval 2022-01-26 11:48:34 +02:00
b7ec1a42be conditionally render network settings button 2022-01-26 11:47:44 +02:00
445c05e3ee style horizontal lines 2022-01-26 11:46:49 +02:00
476eaa540e add managed state for conditional template element rendering 2022-01-26 11:46:14 +02:00
6f03063f8d add managed state for conditional template element rendering 2022-01-26 11:45:59 +02:00
de9b8f5d73 serialize sbot stat struct 2022-01-26 11:45:25 +02:00
51eff6a298 add sbot status-related icons 2022-01-26 11:44:25 +02:00
1c90e45f11 merge latest changes from main 2022-01-25 11:48:32 +02:00
178af281ed Merge pull request 'Retrieve go-sbot systemd process statistics' (#76) from sbot_stats into main
Reviewed-on: #76
2022-01-25 09:46:50 +00:00
e1aa7b1bb6 add sbot docs and bump version 2022-01-25 11:39:28 +02:00
816d6c8a73 add sbot process stats function, struct and error variants 2022-01-25 11:36:24 +02:00
b098f73a5f Merge pull request 'Load values from Rocket.toml into managed state' (#74) from rocket_config into main
Reviewed-on: #74
2022-01-25 09:35:39 +00:00
2bfba66dab restructure auth mode check 2022-01-18 17:12:32 +02:00
43344566de read values from managed state 2022-01-18 17:00:53 +02:00
bfb53747db update rocket config file and related docs 2022-01-18 16:59:54 +02:00
d0321d17d0 remove lazy_static dependency 2022-01-18 16:59:44 +02:00
f3ddbcf07c set auth request guard from managed state 2022-01-18 16:59:03 +02:00
680044cba8 read config params from figment and attach managed state 2022-01-18 16:58:13 +02:00
66555f19bf Merge pull request 'Deduplicate routes and add Scuttlebutt status route & template' (#73) from add_sbot_status into main
Reviewed-on: #73
2022-01-18 10:55:43 +00:00
792779f60f deduplicate mounting of routes 2022-01-18 12:50:06 +02:00
44b68a8b71 register scuttlebutt status routes and pass standalone var to home template 2022-01-18 12:49:47 +02:00
205dd145b4 add links to templates for sbot status page 2022-01-18 12:48:40 +02:00
7346c37c86 define route and template for sbot status 2022-01-18 12:48:11 +02:00
72fbbe83f0 Merge pull request 'Add sbot configuration route and template' (#71) from sbot_settings into main
Reviewed-on: #71
2022-01-18 09:08:52 +00:00
b4a930e774 add bottom margin for small label class 2022-01-17 15:21:11 +02:00
8c4cf6261e add sbot config template and rename menu 2022-01-17 15:20:43 +02:00
8f5b257ed1 mount routes for sbot config 2022-01-17 15:20:08 +02:00
3bb00c4eb7 add route for sbot config 2022-01-17 15:19:48 +02:00
5d75aebf0d Merge pull request 'Wide range of web improvements' (#70) from web_improvements into main
Reviewed-on: #70
2022-01-17 09:35:29 +00:00
4d2a3771b8 remove static/js from cargo-deb assetts 2022-01-16 17:41:49 -05:00
ed6da528a2 remove noscript, update urls 2022-01-14 15:32:37 +02:00
aca687974a fix url for redirect 2022-01-14 15:31:40 +02:00
6e4b8faf40 improve error msg 2022-01-14 15:31:22 +02:00
552c4b419e comment-out system call-invoking function 2022-01-14 15:30:54 +02:00
6fb4a2406b add docs about standalone mode config 2022-01-14 15:30:33 +02:00
65dbc6bdd4 remove noscript template snippet 2022-01-14 15:30:16 +02:00
dbab6f1762 Merge pull request 'Readd peach-menu to peach-config' (#69) from peach-menu into main
Reviewed-on: #69
2022-01-13 17:31:19 +00:00
d3ae25934c Readd peach-buttons and peach-oled 2022-01-13 12:30:46 -05:00
c6f68de516 Fix version numbers 2022-01-13 12:22:38 -05:00
2ccd7e65d3 Readd peach-menu 2022-01-13 12:21:51 -05:00
bf3325a41e Merge pull request 'Remove unused microservices from peach-config' (#68) from update-peach-config into main
Reviewed-on: #68
2022-01-13 16:05:31 +00:00
e4b3479417 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main3 2022-01-13 10:24:19 -05:00
0561b6a9be Remove unused microservices from peach-config 2022-01-13 10:24:10 -05:00
166f4d25ae move context objects and builders to dedicated directory 2022-01-13 15:49:12 +02:00
a5f0d991fa fix template rendering for help 2022-01-13 15:48:55 +02:00
60a0d7f293 set global vars for iface names 2022-01-13 15:47:43 +02:00
d8c40e0724 move context builders into dedicated directory 2022-01-13 15:47:14 +02:00
f4ad230d58 remove unnecessary context objects 2022-01-13 13:16:38 +02:00
b0b21ad8a0 add standalone check before mounting routes 2022-01-13 13:15:42 +02:00
08ee9cd776 cargo fmt 2022-01-12 20:21:39 +02:00
cfd50ca359 cleanup paths and add whitespace 2022-01-12 20:21:05 +02:00
fd94ba27ac replace snafu with custom error impl 2022-01-12 19:58:49 +02:00
bb5cd0f0d3 remove unneeded dependencies 2022-01-12 19:54:30 +02:00
72b7281587 remove json api tests 2022-01-12 19:51:08 +02:00
cbb4027099 Merge pull request 'Add update and forget network functions' (#67) from add_network_functions into main
Reviewed-on: #67
2022-01-12 15:38:09 +00:00
5e1520aa3f merge latest changes from main 2022-01-12 17:36:55 +02:00
a8f3730b7c Merge pull request 'Satisfy clippy warnings' (#66) from satisfy_clippy_web into main
Reviewed-on: #66
2022-01-12 15:36:16 +00:00
c1432bd29e Merge branch 'main' into satisfy_clippy_web 2022-01-12 17:35:11 +02:00
eb77290a93 Merge pull request 'Remove json routes, utils and javascript' (#65) from remove_json_js into main
Reviewed-on: #65
2022-01-12 15:33:20 +00:00
5dcba8e2ad add update and forget functions 2022-01-12 13:39:38 +02:00
69ba400b69 satisfy clippy nightly warnings 2022-01-12 13:15:04 +02:00
2a7c893d94 bump version 2022-01-12 13:08:30 +02:00
2135ab1a5b remove json routes, utils and javascript 2022-01-12 13:04:47 +02:00
6f5cefa367 Merge pull request 'Replace miniserde_support with serde_support for peach-jsonrpc-server' (#64) from fix_workspace_comp into main
Reviewed-on: #64
2022-01-12 10:25:01 +00:00
c6f8591600 replace miniserde_support with serde_support 2022-01-12 12:21:12 +02:00
cd1fb697f7 Merge pull request 'Fig regression of peach-dyndns-updater' (#63) from fix-regression into main
Reviewed-on: #63
2022-01-12 09:58:46 +00:00
a5415aad99 merge direct_call_net_stats branch 2022-01-12 11:35:55 +02:00
037e5c34b6 merge replace_rust_crypto branch 2022-01-12 11:21:18 +02:00
699f2b13c9 merge update_network_args branch 2022-01-12 11:19:24 +02:00
c3fbc5cd73 try operator for dyns dns domain check 2022-01-12 10:59:13 +02:00
4a27892ab6 idiomatic paths and result type for checking new dns address 2022-01-12 10:58:36 +02:00
4adf5547c9 formatting 2022-01-12 10:58:11 +02:00
bdfbd7057f Remove commented out code 2022-01-11 18:10:36 -05:00
171d051710 Fix clippy warmings 2022-01-11 18:06:51 -05:00
1ea0ea2ed1 Fix regression of peach-dyndns-updater 2022-01-11 18:03:07 -05:00
c75608fb1a Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main3 2021-12-22 12:19:56 -05:00
b8f394b901 Debugging dyndns 2021-12-22 09:59:20 -05:00
29cc40be48 Fix setup of nsupdate 2021-12-18 11:24:43 -05:00
271 changed files with 11561 additions and 12977 deletions

33
.drone.yml Normal file
View File

@ -0,0 +1,33 @@
kind: pipeline
type: docker
name: test-on-amd64
platform:
arch: amd64
steps:
- name: rustfmt
image: rust:buster
commands:
- rustup component add rustfmt
- cargo fmt --check
- name: clippy
image: rust:buster
commands:
- rustup component add clippy
- cargo clippy -- -D warnings
- name: test
image: rust:buster
commands:
- cargo test
- name: build
image: rust:buster
commands:
- cargo build
trigger:
event:
- pull_request

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
target
*peachdeploy.sh
*vpsdeploy.sh
*bindeploy.sh

3175
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
[workspace]
members = [
"peach-buttons",
"peach-oled",
"peach-lib",
"peach-config",
@ -11,5 +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,58 +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
## 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
## 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)

View File

@ -0,0 +1,34 @@
---
name: "Bug Report Template"
about: "This template is for submitting bugs."
title: "[BUG] "
ref: "main"
labels:
- bug
- "help needed"
---
> Please fill out the sections below.
> Be kind and objective when writing in text.
> Thanks for the report! :)
**Brief description of the bug:**
**Steps to reproduce the bug:**
**Expected behaviour:**
**Technical details:**
_Is peach-web running on an x86-64 or arm64 machine?_
_What operating system distribution is it running on?_

View File

@ -0,0 +1,15 @@
---
name: "Feature Suggestion Template"
about: "This template is for submitting feature suggestions."
title: "[FEATURE] "
ref: "main"
labels:
- enhancement
---
**Brief description of the feature you'd like to suggest:**

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.15"
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"
@ -37,3 +37,5 @@ log = "0.4"
lazy_static = "1.4.0"
peach-lib = { path = "../peach-lib" }
rpassword = "5.0"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
async-std = "1.10.0"

View File

@ -3,17 +3,14 @@
pub const CONF: &str = "/var/lib/peachcloud/conf";
// List of package names which are installed via apt-get
pub const SERVICES: [&str; 11] = [
"peach-oled",
"peach-network",
"peach-stats",
pub const SERVICES: [&str; 8] = [
"peach-web",
"peach-probe",
"peach-menu",
"peach-buttons",
"peach-monitor",
"peach-probe",
"peach-oled",
"peach-dyndns-updater",
"peach-go-sbot",
"go-sbot",
"peach-config",
];

View File

@ -1,4 +1,5 @@
#![allow(clippy::nonstandard_macro_braces)]
use golgi::error::GolgiError;
use peach_lib::error::PeachError;
pub use snafu::ResultExt;
use snafu::Snafu;
@ -35,6 +36,14 @@ pub enum PeachConfigError {
ChangePasswordError { source: PeachError },
#[snafu(display("Entered passwords did not match. Please try again."))]
InvalidPassword,
#[snafu(display("Error in peach lib: {}", source))]
PeachLibError { source: PeachError },
#[snafu(display("Error in golgi: {}", source))]
Golgi { source: GolgiError },
#[snafu(display("{}", message))]
CmdInputError { message: String },
#[snafu(display("{}", message))]
WaitForSbotError { message: String },
}
impl From<std::io::Error> for PeachConfigError {
@ -51,3 +60,15 @@ impl From<serde_json::Error> for PeachConfigError {
PeachConfigError::SerdeError { source: err }
}
}
impl From<PeachError> for PeachConfigError {
fn from(err: PeachError) -> PeachConfigError {
PeachConfigError::PeachLibError { source: err }
}
}
impl From<GolgiError> for PeachConfigError {
fn from(err: GolgiError) -> PeachConfigError {
PeachConfigError::Golgi { source: err }
}
}

View File

@ -1,40 +1,32 @@
use regex::Regex;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use std::collections::HashMap;
use std::fs;
use crate::constants::HARDWARE_CONFIG_FILE;
use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES};
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
use crate::utils::get_output;
use crate::RtcOption;
/// Helper function which returns the version of a package currently installed,
/// as an Ok(String) if found, and as an Err if not found
pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> {
let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?;
Ok(version)
}
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
{
// gets a list of all packages currently installed with dpkg
let packages = get_output(&["dpkg", "-l"])?;
// this regex matches packages which contain the word peach in them
// and has two match groups
// 1. the first match group gets the package name
// 2. the second match group gets the version number of the package
let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
// the following iterator, iterates through the captures matched via the regex
// and for each capture, creates a value in the hash map,
// which maps the name of the package, to its version number
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
let peach_packages: HashMap<String, String> = re
.captures_iter(&packages)
.filter_map(|cap| {
let groups = (cap.get(1), cap.get(2));
match groups {
(Some(package), Some(version)) => {
Some((package.as_str().to_string(), version.as_str().to_string()))
}
_ => None,
// gets a list of all packages currently installed with dpkg-query
let peach_packages: HashMap<String, String> = SERVICES
.iter()
.filter_map(|service| {
let version = get_package_version_number(service);
match version {
Ok(v) => Some((service.to_string(), v)),
Err(_) => None,
}
})
.collect();

View File

@ -2,12 +2,15 @@ mod change_password;
mod constants;
mod error;
mod generate_manifest;
mod publish_address;
mod set_permissions;
mod setup_networking;
mod setup_peach;
mod setup_peach_deb;
mod status;
mod update;
mod utils;
mod wait_for_sbot;
use clap::arg_enum;
use log::error;
@ -44,12 +47,25 @@ enum PeachConfig {
Update(UpdateOpts),
/// Changes the password for the peach-web interface
#[structopt(name = "changepassword")]
#[structopt(name = "change-password")]
ChangePassword(ChangePasswordOpts),
/// Updates file permissions on PeachCloud device
#[structopt(name = "permissions")]
SetPermissions,
/// Returns sbot id if sbot is running
#[structopt(name = "whoami")]
WhoAmI,
/// Publish domain and port.
/// 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)]
@ -90,6 +106,13 @@ pub struct ChangePasswordOpts {
password: Option<String>,
}
#[derive(StructOpt, Debug)]
pub struct PublishAddressOpts {
/// Specify address in the form domain:port
#[structopt(short, long)]
address: String,
}
arg_enum! {
/// enum options for real-time clock choices
#[derive(Debug)]
@ -102,7 +125,7 @@ arg_enum! {
}
}
fn main() {
async fn run() {
// initialize the logger
env_logger::init();
@ -155,6 +178,42 @@ fn main() {
)
}
},
PeachConfig::WhoAmI => match status::whoami().await {
Ok(sbot_id) => {
println!("{:?}", sbot_id);
{}
}
Err(err) => {
error!("sbot whoami encountered an error: {}", err)
}
},
PeachConfig::PublishAddress(opts) => {
match publish_address::publish_address(opts.address).await {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error during publish address: {}",
err
)
}
}
}
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)
}
},
}
}
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
run().await;
}

View File

@ -0,0 +1,37 @@
use crate::error::PeachConfigError;
use golgi::kuska_ssb::api::dto::content::PubAddress;
use golgi::messages::SsbMessageContent;
use peach_lib::sbot::init_sbot;
/// Utility function to publish the address (domain:port) of the pub
/// publishing the address causes the domain and port to be used for invite generation,
/// and also gossips this pub address to their peers
pub async fn publish_address(address: String) -> Result<(), PeachConfigError> {
// split address into domain:port
let split: Vec<&str> = address.split(':').collect();
let (domain, port): (&str, &str) = (split[0], split[1]);
// convert port to u16
let port_as_u16: u16 = port
.parse()
.map_err(|_err| PeachConfigError::CmdInputError {
message: "Failure to parse domain and port. Address must be of the format host:port."
.to_string(),
})?;
// publish address
let mut sbot = init_sbot().await?;
let pub_id = sbot.whoami().await?;
// Compose a `pub` address type message.
let pub_address_msg = SsbMessageContent::Pub {
address: Some(PubAddress {
// Host name (can be an IP address if onboarding over WiFi).
host: Some(domain.to_string()),
// Port.
port: port_as_u16,
// Public key.
key: pub_id,
}),
};
// Publish the `pub` address message.
let _pub_msg_ref = sbot.publish(pub_address_msg).await?;
Ok(())
}

View File

@ -1,21 +1,30 @@
use lazy_static::lazy_static;
use peach_lib::config_manager;
use crate::error::PeachConfigError;
use crate::utils::cmd;
/// All configs are stored in this folder, and should be read/writeable by peach group
/// so they can be read and written by all PeachCloud services.
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
lazy_static! {
pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR")
.expect("Failed to load config value for PEACH_CONFIGDIR");
pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR")
.expect("Failed to load config value for PEACH_WEBDIR");
pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR")
.expect("Failed to load config value for PEACH_HOMEDIR");
}
/// Utility function to set correct file permissions on the PeachCloud device.
/// Accidentally changing file permissions is a fairly common thing to happen,
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
pub fn set_permissions() -> Result<(), PeachConfigError> {
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?;
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
Ok(())
}

View File

@ -4,6 +4,7 @@ use std::fs;
use crate::error::{FileWriteError, PeachConfigError};
use crate::generate_manifest::save_hardware_config;
use crate::set_permissions::set_permissions;
use crate::setup_networking::configure_networking;
use crate::setup_peach_deb::setup_peach_deb;
use crate::update::update_microservices;
@ -68,6 +69,7 @@ pub fn setup_peach(
"libssl-dev",
"nginx",
"wget",
"dnsutils",
"-y",
])?;
@ -238,6 +240,9 @@ pub fn setup_peach(
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
save_hardware_config(i2c, rtc)?;
info!("[ SETTING FILE PERMISSIONS ]");
set_permissions()?;
info!("[ PEACHCLOUD SETUP COMPLETE ]");
info!("[ ------------------------- ]");
info!("[ please reboot your device ]");

View File

@ -0,0 +1,9 @@
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
/// Utility function to check if sbot is running via the whoami method
pub async fn whoami() -> Result<String, PeachConfigError> {
let mut sbot = init_sbot().await?;
let sbot_id = sbot.whoami().await?;
Ok(sbot_id)
}

View File

@ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> {
cmd(&["apt-get", "update"])?;
// filter out peach-config from list of services
let services_to_update: Vec<&str> = SERVICES
.to_vec()
.into_iter()
.iter()
.copied()
.filter(|&x| x != "peach-config")
.collect();

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "peach-dyndns-updater"
version = "0.1.6"
version = "0.1.8"
authors = ["Max Fowler <mfowler@commoninternet.net>"]
edition = "2018"
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."

View File

@ -1,6 +1,5 @@
use log::info;
use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() {
// initalize the logger

View File

@ -18,8 +18,8 @@ env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
miniserde = "0.1.15"
peach-stats = { path = "../peach-stats", features = ["miniserde_support"] }
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
serde_json = "1.0.74"
[dev-dependencies]
jsonrpc-test = "18"

View File

@ -1,12 +1,16 @@
use std::fmt;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use serde_json::error::Error as SerdeJsonError;
use peach_stats::StatsError;
/// Custom error type encapsulating all possible errors for a JSON-RPC server
/// and associated methods.
#[derive(Debug)]
pub enum JsonRpcServerError {
/// Failed to serialize a string from a data structure.
Serde(SerdeJsonError),
/// An error returned from the `peach-stats` library.
Stats(StatsError),
/// An expected JSON-RPC method parameter was not provided.
@ -24,6 +28,9 @@ impl fmt::Display for JsonRpcServerError {
JsonRpcServerError::MissingParameter(ref source) => {
write!(f, "Missing expected parameter: {}", source)
}
JsonRpcServerError::Serde(ref source) => {
write!(f, "{}", source)
}
JsonRpcServerError::Stats(ref source) => {
write!(f, "{}", source)
}
@ -34,6 +41,11 @@ impl fmt::Display for JsonRpcServerError {
impl From<JsonRpcServerError> for JsonRpcError {
fn from(err: JsonRpcServerError) -> Self {
match &err {
JsonRpcServerError::Serde(source) => JsonRpcError {
code: ErrorCode::ServerError(-32002),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::Stats(source) => JsonRpcError {
code: ErrorCode::ServerError(-32001),
message: format!("{}", source),

View File

@ -8,7 +8,6 @@ use std::result::Result;
use jsonrpc_core::{IoHandler, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info;
use miniserde::json;
use peach_stats::stats;
mod error;
@ -30,7 +29,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("cpu_stats", move |_| {
info!("Fetching CPU statistics.");
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
let json_cpu = json::to_string(&cpu);
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_cpu))
});
@ -38,7 +37,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("cpu_stats_percent", move |_| {
info!("Fetching CPU statistics as percentages.");
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
let json_cpu = json::to_string(&cpu);
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_cpu))
});
@ -46,7 +45,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("disk_usage", move |_| {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
let json_disks = json::to_string(&disks);
let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_disks))
});
@ -54,7 +53,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("load_average", move |_| {
info!("Fetching system load average statistics.");
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
let json_avg = json::to_string(&avg);
let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_avg))
});
@ -62,7 +61,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("mem_stats", move |_| {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
let json_mem = json::to_string(&mem);
let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_mem))
});
@ -70,7 +69,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("uptime", move |_| {
info!("Fetching system uptime.");
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
let json_uptime = json::to_string(&uptime);
let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_uptime))
});

View File

@ -1,19 +1,27 @@
[package]
name = "peach-lib"
version = "1.3.1"
version = "1.3.4"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
[dependencies]
chrono = "0.4.19"
fslock="0.1.6"
async-std = "1.10"
chrono = "0.4"
dirs = "4.0"
fslock="0.1"
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.1"
jsonrpc-core = "8.0"
jsonrpc_client = "0.7"
log = "0.4"
nanorand = "0.6.1"
nanorand = { version = "0.6", features = ["getrandom"] }
regex = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
sha3 = "0.10.0"
toml = "0.5"
sha3 = "0.10"
lazy_static = "1.4"
anyhow = "1.0.86"

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,12 +0,0 @@
[package]
name = "debug"
version = "0.1.0"
authors = ["notplants <mfowler.email@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
peach-lib = { path = "../" }
env_logger = "0.6"
chrono = "0.4.19"

View File

@ -1,65 +0,0 @@
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
use peach_lib::sbot_client;
use std::process;
use chrono::prelude::*;
fn main() {
// initalize the logger
env_logger::init();
//
// println!("Hello, world its debug!");
// let result = set_new_password("password3");
// println!("result: {:?}", result);
//
// let result = verify_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_password("password3");
// println!("result should be ok: {:?}", result);
//
//
// println!("Testing temporary passwords");
// let result = set_new_temporary_password("abcd");
// println!("result: {:?}", result);
//
// let result = verify_temporary_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_temporary_password("abcd");
// println!("result should be ok: {:?}", result);
//
let result = send_password_reset();
println!("send password reset result should be ok: {:?}", result);
// sbot_client::post("hi cat");
// let result = sbot_client::whoami();
// let result = sbot_client::create_invite(50);
// let result = sbot_client::post("is this working");
// println!("result: {:?}", result);
// let result = sbot_client::post("nice we have contact");
// let result = sbot_client::update_pub_name("vermont-pub");
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
// println!("result: {:?}", result);
// let result = send_password_reset();
// let result = add_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("ab");
// println!("result: {:?}", result);
//// let result = log_successful_nsupdate();
//// let result = get_num_seconds_since_successful_dns_update();
// let is_online = is_dns_updater_online();
// println!("is online: {:?}", is_online);
//
//// let result = get_last_successful_dns_update();
//// println!("result: {:?}", result);
//// register_domain("newquarter299.dyn.peachcloud.org");
// let result = dyndns_update_ip();
// println!("result: {:?}", result);
}

View File

@ -0,0 +1,11 @@
use peach_lib::config_manager::{get_config_value, save_config_value};
fn main() {
println!("Running example of PeachCloud configuration management");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
save_config_value("ADDR", "1.1.1.1");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
}

View File

@ -1,148 +1,283 @@
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
//!
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
//! Different PeachCloud microservices import peach-lib, so that they can share
//! this interface.
//!
//! Config values are looked up from three locations in this order by key name:
//! 1. from environmental variables
//! 2. from a configuration file
//! 3. from default values
//!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
use std::fs;
use std::collections::{BTreeMap, HashMap};
use std::{env, fs};
use fslock::LockFile;
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use log::debug;
use crate::error::PeachError;
// main configuration file
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
// load path to main configuration file
// from PEACH_CONFIG_PATH if that environment variable is set
// or using the default value if not set
pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
lazy_static! {
static ref CONFIG_PATH: String = {
if let Ok(val) = env::var("PEACH_CONFIG_PATH") {
val
}
else {
DEFAULT_YAML_PATH.to_string()
}
};
// lock file (used to avoid race conditions during config reading & writing)
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
// we make use of Serde default values in order to make PeachCloud
// robust and keep running even with a not fully complete config.yml
// main type which represents all peachcloud configurations
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct PeachConfig {
#[serde(default)]
pub external_domain: String,
#[serde(default)]
pub dyn_domain: String,
#[serde(default)]
pub dyn_dns_server_address: String,
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
#[serde(default)] // default is empty vector
pub ssb_admin_ids: Vec<String>,
#[serde(default)]
pub admin_password_hash: String,
#[serde(default)]
pub temporary_password_hash: String,
// the lock file path is the config file path + ".lock"
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH);
}
// helper functions for serializing and deserializing PeachConfig from disc
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
// Default values for PeachCloud configs which are used for any key which is not set
// via an environment variable or in a saved configuration file.
pub fn get_peach_config_defaults() -> HashMap<String, String> {
let peach_config_defaults: HashMap<&str, &str> = HashMap::from([
("STANDALONE_MODE", "true"),
("DISABLE_AUTH", "false"),
("ADDR", "127.0.0.1"),
("PORT", "8000"),
("EXTERNAL_DOMAIN", ""),
("DYN_DOMAIN", ""),
(
"DYN_DNS_SERVER_ADDRESS",
"http://dynserver.dyn.peachcloud.org",
),
("DYN_USE_CUSTOM_SERVER", "true"),
("DYN_TSIG_KEY_PATH", ""),
("DYN_NAMESERVER", "ns.peachcloud.org"),
("DYN_ENABLED", "false"),
("SSB_ADMIN_IDS", ""),
("ADMIN_PASSWORD_HASH", "47"),
("TEMPORARY_PASSWORD_HASH", ""),
("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"),
]);
// convert HashMap<&str, &str> to HashMap<String, String> and return
let pc_defaults: HashMap<String, String> = peach_config_defaults
.iter()
.map(|(key, val)| (key.to_string(), val.to_string()))
.collect();
pc_defaults
}
// primary interface for getting config values
// Config values are looked up from three locations in this order by key name:
// 1. from environmental variables
// 2. from a configuration file
// 3. from default values
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
// first check if there is an environmental variable set
if let Ok(val) = env::var(key) {
Ok(val)
} else {
// then check if a value is set in the config file
let peach_config_on_disc = load_peach_config_from_disc()?;
let val = peach_config_on_disc.get(key);
// if no value is found in the config file, then get the default value
match val {
// return config value
Some(v) => Ok(v.to_string()),
// get default value
None => {
match get_peach_config_defaults().get(key) {
Some(v) => Ok(v.to_string()),
// if this key was not found in the defaults, then it was an invalid key
None => Err(PeachError::InvalidKey {
key: key.to_string(),
}),
}
}
}
}
}
// helper function to load PeachCloud configuration file saved to disc
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists();
// if config file does not exist, return an emtpy HashMap
if !peach_config_exists {
let peach_config: HashMap<String, String> = HashMap::new();
Ok(peach_config)
}
// otherwise we load peach config from disk
else {
debug!("Loading peach config: {} exists", CONFIG_PATH.as_str());
let contents =
fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read {
source,
path: CONFIG_PATH.to_string(),
})?;
let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?;
Ok(peach_config)
}
}
// helper function to save PeachCloud configuration file to disc
// takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file
// with the keys in alphabetical order
pub fn save_peach_config_to_disc(
peach_config: HashMap<String, String>,
) -> Result<HashMap<String, String>, PeachError> {
// use a file lock to avoid race conditions while saving config
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read {
source,
path: LOCK_FILE_PATH.to_string(),
})?;
lock.lock()?;
let yaml_str = serde_yaml::to_string(&peach_config)?;
// first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order)
let ordered: BTreeMap<_, _> = peach_config.iter().collect();
// then serialize BTreeMap as yaml
let yaml_str = serde_yaml::to_string(&ordered)?;
fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write {
// write yaml to file
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
source,
path: YAML_PATH.to_string(),
path: CONFIG_PATH.to_string(),
})?;
// unlock file lock
lock.unlock()?;
// return peach_config
// return modified HashMap
Ok(peach_config)
}
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
// helper functions for serializing and deserializing PeachConfig values from disc
pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> {
// get current config from disc
let mut peach_config = load_peach_config_from_disc()?;
let peach_config: PeachConfig;
// insert new key/value
peach_config.insert(key.to_string(), value.to_string());
// if this is the first time loading peach_config, we can create a default here
if !peach_config_exists {
peach_config = PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: "".to_string(),
dyn_tsig_key_path: "".to_string(),
dyn_enabled: false,
ssb_admin_ids: Vec::new(),
admin_password_hash: "".to_string(),
temporary_password_hash: "".to_string(),
};
}
// otherwise we load peach config from disk
else {
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
source,
path: YAML_PATH.to_string(),
})?;
peach_config = serde_yaml::from_str(&contents)?;
// save the modified hashmap to disc
save_peach_config_to_disc(peach_config)
}
Ok(peach_config)
}
// interfaces for setting specific config values
// set all dyn configuration values at once
pub fn set_peach_dyndns_config(
dyn_domain: &str,
dyn_dns_server_address: &str,
dyn_tsig_key_path: &str,
dyn_enabled: bool,
) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_domain = dyn_domain.to_string();
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
peach_config.dyn_enabled = dyn_enabled;
save_peach_config(peach_config)
) -> Result<HashMap<String, String>, PeachError> {
let mut peach_config = load_peach_config_from_disc()?;
let dyn_enabled_str = match dyn_enabled {
true => "true",
false => "false",
};
peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string());
peach_config.insert(
"DYN_DNS_SERVER_ADDRESS".to_string(),
dyn_dns_server_address.to_string(),
);
peach_config.insert(
"DYN_TSIG_KEY_PATH".to_string(),
dyn_tsig_key_path.to_string(),
);
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
save_peach_config_to_disc(peach_config)
}
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.external_domain = new_external_domain.to_string();
save_peach_config(peach_config)
pub fn set_external_domain(
new_external_domain: &str,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("EXTERNAL_DOMAIN", new_external_domain)
}
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.external_domain.is_empty() {
Ok(Some(peach_config.external_domain))
} else if !peach_config.dyn_domain.is_empty() {
Ok(Some(peach_config.dyn_domain))
let external_domain = get_config_value("EXTERNAL_DOMAIN")?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
if !external_domain.is_empty() {
Ok(Some(external_domain))
} else if !dyn_domain.is_empty() {
Ok(Some(dyn_domain))
} else {
Ok(None)
}
}
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_enabled = enabled_value;
save_peach_config(peach_config)
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
get_config_value("DYN_DNS_SERVER_ADDRESS")
}
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.ssb_admin_ids.push(ssb_id.to_string());
save_peach_config(peach_config)
pub fn set_dyndns_enabled_value(
enabled_value: bool,
) -> Result<HashMap<String, String>, PeachError> {
match enabled_value {
true => save_config_value("DYN_ENABLED", "true"),
false => save_config_value("DYN_ENABLED", "false"),
}
}
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> {
let val = get_config_value("DYN_ENABLED")?;
Ok(val == "true")
}
pub fn set_admin_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("ADMIN_PASSWORD_HASH", &password_hash)
}
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
pub fn set_temporary_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash)
}
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
// add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
ssb_admin_ids.push(ssb_id.to_string());
save_ssb_admin_ids(ssb_admin_ids)
}
// remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS
// if value is not found then return an error
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
match index_result {
Some(index) => {
ssb_admin_ids.remove(index);
peach_config.ssb_admin_ids = ssb_admin_ids;
save_peach_config(peach_config)
save_ssb_admin_ids(ssb_admin_ids)
}
None => Err(PeachError::SsbAdminIdNotFound {
id: ssb_id.to_string(),
@ -150,32 +285,20 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
}
}
pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.admin_password_hash = password_hash.to_string();
save_peach_config(peach_config)
}
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.admin_password_hash.is_empty() {
Ok(peach_config.admin_password_hash)
// 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")?;
if ssb_admin_ids_str.trim().is_empty() {
Ok(vec![])
} else {
Err(PeachError::PasswordNotSet)
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
Ok(ssb_admin_ids)
}
}
pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.temporary_password_hash = password_hash.to_string();
save_peach_config(peach_config)
}
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.temporary_password_hash.is_empty() {
Ok(peach_config.temporary_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec
pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> {
let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?;
save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?;
Ok(ssb_admin_ids)
}

View File

@ -9,13 +9,8 @@
//!
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
use std::{
fs,
fs::OpenOptions,
io::Write,
process::{Command, Stdio},
str::FromStr,
};
use std::ffi::OsStr;
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
@ -23,13 +18,12 @@ use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use regex::Regex;
use crate::{
config_manager::{load_peach_config, set_peach_dyndns_config},
error::PeachError,
use crate::config_manager::{
get_config_value, get_dyndns_enabled_value, get_dyndns_server_address,
};
use crate::{config_manager, error::PeachError};
/// constants for dyndns configuration
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
@ -62,9 +56,10 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
let http_server = get_dyndns_server_address()?;
info!("Using dyndns http server address: {:?}", http_server);
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach-dyndns service.");
let mut client = PeachDynDnsClient::new(transport_handle);
@ -73,7 +68,8 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
// save new TSIG key
save_dyndns_key(&key)?;
// save new configuration values
let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
let set_config_result =
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
match set_config_result {
Ok(_) => {
let response = "success".to_string();
@ -87,9 +83,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
let http_server = get_dyndns_server_address()?;
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachDynDnsClient::new(transport_handle);
@ -105,7 +101,7 @@ pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError
/// Helper function to get public ip address of PeachCloud device.
fn get_public_ip_address() -> Result<String, PeachError> {
// TODO: consider other ways to get public IP address
let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?;
let output = Command::new("curl").arg("ifconfig.me").output()?;
let command_output = String::from_utf8(output.stdout)?;
Ok(command_output)
}
@ -113,31 +109,28 @@ fn get_public_ip_address() -> Result<String, PeachError> {
/// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
info!("Running dyndns_update_ip");
let peach_config = load_peach_config()?;
let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?;
let dyn_enabled = get_dyndns_enabled_value()?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
info!(
"Using config:
dyn_tsig_key_path: {:?}
dyn_domain: {:?}
dyn_dns_server_address: {:?}
dyn_enabled: {:?}
dyn_nameserver: {:?}
",
peach_config.dyn_tsig_key_path,
peach_config.dyn_domain,
peach_config.dyn_dns_server_address,
peach_config.dyn_enabled,
dyn_tsig_key_path, dyn_domain, dyn_dns_server_address, dyn_enabled, dyn_nameserver,
);
if !peach_config.dyn_enabled {
if !dyn_enabled {
info!("dyndns is not enabled, not updating");
Ok(false)
} else {
// call nsupdate passing appropriate configs
let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
.arg("-k")
.arg(&peach_config.dyn_tsig_key_path)
.arg("-v")
.stdin(Stdio::piped())
.spawn()?;
let mut nsupdate_command = Command::new("nsupdate");
nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v");
// pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?;
info!("found public ip address: {}", public_ip_address);
@ -148,20 +141,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send",
NAMESERVER = "ns.peachcloud.org",
ZONE = peach_config.dyn_domain,
DOMAIN = peach_config.dyn_domain,
NAMESERVER = dyn_nameserver,
ZONE = dyn_domain,
DOMAIN = dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address,
);
let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
})?;
write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
source,
path: peach_config.dyn_tsig_key_path.to_string(),
})?;
let nsupdate_output = nsupdate_command.wait_with_output()?;
info!("nsupdate output: {:?}", nsupdate_output);
info!("ns_commands: {:?}", ns_commands);
info!("creating nsupdate temp file");
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
// write ns_commands to temp_file
fs::write(temp_file_path, ns_commands)?;
nsupdate_command.arg(temp_file_path);
let nsupdate_output = nsupdate_command.output()?;
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
info!("nsupdate command: {:?}", args);
// We only return a successful result if nsupdate was successful
if nsupdate_output.status.success() {
info!("nsupdate succeeded, returning ok");
@ -204,7 +197,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
})?;
// replace newline if found
// TODO: maybe we can use `.trim()` instead
let contents = contents.replace("\n", "");
let contents = contents.replace('\n', "");
// TODO: consider adding additional context?
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
PeachError::ParseDateTime {
@ -223,20 +216,14 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
/// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config
let peach_config = load_peach_config()?;
let is_enabled = peach_config.dyn_enabled;
let is_enabled = get_dyndns_enabled_value()?;
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool;
match num_seconds_since_successful_update {
Some(seconds) => {
ran_recently = seconds < (60 * 6);
}
let ran_recently: bool = match num_seconds_since_successful_update {
Some(seconds) => seconds < (60 * 6),
// if the value is None, then the last time it ran successfully is unknown
None => {
ran_recently = false;
}
}
None => false,
};
// debug log
info!("is_dyndns_enabled: {:?}", is_enabled);
info!("dyndns_ran_recently: {:?}", ran_recently);
@ -258,11 +245,9 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
}
// helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
// TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
let peach_config = load_peach_config().unwrap();
let previous_dyndns_domain = peach_config.dyn_domain;
dyndns_full_domain != previous_dyndns_domain
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?;
Ok(dyndns_full_domain != previous_dyndns_domain)
}
jsonrpc_client!(pub struct PeachDynDnsClient {

View File

@ -1,12 +1,22 @@
#![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 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)]
pub enum PeachError {
/// Represents looking up a Config value with a non-existent key
InvalidKey {
/// the key value which was invalid
key: String,
},
/// Represents a failure to determine the path of the user's home directory.
HomeDir,
/// Represents all other cases of `std::io::Error`.
Io(io::Error),
@ -58,15 +68,18 @@ pub enum PeachError {
/// Represents a failure to parse or compile a regular expression.
Regex(regex::Error),
/// Represents a failure to successfully execute an sbot command.
SbotCli {
/// The `stderr` output from the sbot command.
msg: String,
},
/// Represents a failure to successfully execute an sbot command (via golgi).
Sbot(String),
/// Represents a failure to serialize or deserialize JSON.
SerdeJson(serde_json::error::Error),
/// Represents a failure to deserialize TOML.
TomlDeser(toml::de::Error),
/// Represents a failure to serialize TOML.
TomlSer(toml::ser::Error),
/// Represents a failure to serialize or deserialize YAML.
SerdeYaml(serde_yaml::Error),
@ -87,14 +100,29 @@ pub enum PeachError {
Write {
/// The underlying source of the error.
source: io::Error,
/// The file path for the write attemp.
/// The file path for the write attempt.
path: String,
},
/// 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 {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachError::HomeDir => None,
PeachError::InvalidKey { .. } => None,
PeachError::Io(_) => None,
PeachError::JsonRpcClientCore(_) => None,
PeachError::JsonRpcCore(_) => None,
@ -107,13 +135,19 @@ impl std::error::Error for PeachError {
PeachError::PasswordNotSet => None,
PeachError::Read { ref source, .. } => Some(source),
PeachError::Regex(_) => None,
PeachError::SbotCli { .. } => None,
PeachError::Sbot(_) => None,
PeachError::SerdeJson(_) => None,
PeachError::SerdeYaml(_) => None,
PeachError::SsbAdminIdNotFound { .. } => None,
PeachError::TomlDeser(_) => None,
PeachError::TomlSer(_) => None,
PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source),
PeachError::JsonRpcError(_) => None,
PeachError::SolarClientError(_) => None,
PeachError::TildeClientError(_) => None,
PeachError::SsbMessageError(_) => None,
}
}
}
@ -121,6 +155,15 @@ impl std::error::Error for PeachError {
impl std::fmt::Display for PeachError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachError::InvalidKey { ref key } => {
write!(f, "Invalid key in config lookup for key: {}", key)
}
PeachError::HomeDir => {
write!(
f,
"Unable to determine the path of the user's home directory"
)
}
PeachError::Io(ref err) => err.fmt(f),
PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
PeachError::JsonRpcCore(ref err) => {
@ -135,22 +178,19 @@ impl std::fmt::Display for PeachError {
write!(f, "Date/time parse error: {}", path)
}
PeachError::PasswordIncorrect => {
write!(f, "Password error: user-supplied password is incorrect")
write!(f, "password is incorrect")
}
PeachError::PasswordMismatch => {
write!(f, "Password error: user-supplied passwords do not match")
write!(f, "passwords do not match")
}
PeachError::PasswordNotSet => {
write!(
f,
"Password error: hash value in YAML configuration file is empty"
)
write!(f, "hash value in YAML configuration file is empty")
}
PeachError::Read { ref path, .. } => {
write!(f, "Read error: {}", path)
}
PeachError::Regex(ref err) => err.fmt(f),
PeachError::SbotCli { ref msg } => {
PeachError::Sbot(ref msg) => {
write!(f, "Sbot error: {}", msg)
}
PeachError::SerdeJson(ref err) => err.fmt(f),
@ -158,11 +198,17 @@ impl std::fmt::Display for PeachError {
PeachError::SsbAdminIdNotFound { ref id } => {
write!(f, "Config error: SSB admin ID `{}` not found", id)
}
PeachError::TomlDeser(ref err) => err.fmt(f),
PeachError::TomlSer(ref err) => err.fmt(f),
PeachError::Utf8ToStr(ref err) => err.fmt(f),
PeachError::Utf8ToString(ref err) => err.fmt(f),
PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path)
}
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),
}
}
}
@ -209,6 +255,18 @@ impl From<serde_yaml::Error> for PeachError {
}
}
impl From<toml::de::Error> for PeachError {
fn from(err: toml::de::Error) -> PeachError {
PeachError::TomlDeser(err)
}
}
impl From<toml::ser::Error> for PeachError {
fn from(err: toml::ser::Error) -> PeachError {
PeachError::TomlSer(err)
}
}
impl From<str::Utf8Error> for PeachError {
fn from(err: str::Utf8Error) -> PeachError {
PeachError::Utf8ToStr(err)
@ -220,3 +278,16 @@ impl From<string::FromUtf8Error> for PeachError {
PeachError::Utf8ToString(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

@ -4,11 +4,13 @@ pub mod error;
pub mod network_client;
pub mod oled_client;
pub mod password_utils;
pub mod sbot_client;
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,13 +1,16 @@
use async_std::task;
use log::debug;
use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crate::{config_manager, error::PeachError, sbot_client};
use crate::sbot::init_sbot;
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> {
let real_admin_password_hash = config_manager::get_admin_password_hash()?;
let password_hash = hash_password(&password.to_string());
let password_hash = hash_password(password);
if real_admin_password_hash == password_hash {
Ok(())
} else {
@ -29,8 +32,8 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
/// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_admin_password_hash(&new_password_hash)?;
let new_password_hash = hash_password(new_password);
config_manager::set_admin_password_hash(new_password_hash)?;
Ok(())
}
@ -49,8 +52,8 @@ pub fn hash_password(password: &str) -> String {
/// Sets a new temporary password for the admin user
/// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_temporary_password_hash(&new_password_hash)?;
let new_password_hash = hash_password(new_password);
config_manager::set_temporary_password_hash(new_password_hash)?;
Ok(())
}
@ -59,7 +62,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError>
/// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
let password_hash = hash_password(&password.to_string());
let password_hash = hash_password(password);
if temporary_admin_password_hash == password_hash {
Ok(())
} else {
@ -83,7 +86,7 @@ pub fn send_password_reset() -> Result<(), PeachError> {
"Your new temporary password is: {}
If you are on the same WiFi network as your PeachCloud device you can reset your password \
using this link: http://peach.local/reset_password",
using this link: http://peach.local/auth/reset",
temporary_password
);
// if there is an external domain, then include remote link in message
@ -92,7 +95,7 @@ using this link: http://peach.local/reset_password",
Some(domain) => {
format!(
"\n\nOr if you are on a different WiFi network, you can reset your password \
using the the following link: {}/reset_password",
using the the following link: {}/auth/reset",
domain
)
}
@ -100,9 +103,25 @@ using this link: http://peach.local/reset_password",
};
msg += &remote_link;
// finally send the message to the admins
let peach_config = config_manager::load_peach_config()?;
for ssb_admin_id in peach_config.ssb_admin_ids {
sbot_client::private_message(&msg, &ssb_admin_id)?;
let ssb_admin_ids = config_manager::get_ssb_admin_ids()?;
for ssb_admin_id in ssb_admin_ids {
// 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(e),
}
}
Ok(())
}
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 = init_sbot().await?;
debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.private_message(recipient, msg).await {
Ok(_) => Ok(()),
Err(e) => Err(PeachError::TildeClientError(format!("Failed to publish private message: {}", e))),
}
}

281
peach-lib/src/sbot.rs Normal file
View File

@ -0,0 +1,281 @@
//! Data types and associated methods for monitoring and configuring solar-sbot.
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
use std::os::linux::raw::ino_t;
use tilde_client::{TildeClient};
use log::debug;
use crate::config_manager;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
/* HELPER FUNCTIONS */
// iterate over the given directory path to determine the size of the directory
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
dir.try_fold(0, |acc, file| {
let file = file?;
let size = match file.metadata()? {
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
data => data.len(),
};
Ok(acc + size)
})
}
dir_size(fs::read_dir(path.into())?)
}
/* SBOT-RELATED TYPES AND METHODS */
/// solar-sbot process status.
#[derive(Debug, Serialize, Deserialize)]
pub struct SbotStatus {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
/// Size of the blobs directory in bytes.
pub blobstore: Option<u64>,
}
/// Default builder for `SbotStatus`.
impl Default for SbotStatus {
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
blobstore: None,
}
}
}
impl SbotStatus {
/// 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(service_name)
.arg("--no-page")
.output()?;
let service_info = std::str::from_utf8(&info_output.stdout)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
// note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl status
let status_output = Command::new("systemctl")
.arg("status")
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?;
let service_status = str::from_utf8(&status_output.stdout)?;
//.map_err(PeachError::Utf8ToStr)?;
for line in service_status.lines() {
// example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/solar-sbot.service; enabled; vendor
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
// 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!(
"{}",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
// determine the size of the blobstore directory in bytes
status.blobstore = dir_size(blobstore_path).ok();
Ok(status)
}
}
/// solar-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Config {
// TODO: maybe define as a Path type?
/// Directory path for the log and indexes.
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 replication_hops: u8,
/// Ip address of pub
pub ip: String,
/// Address to listen on.
pub ssb_port: String,
/// Enable sending local UDP broadcasts.
pub localadv: bool,
/// Enable listening for UDP broadcasts and connecting.
pub localdiscov: bool,
/// Enable syncing by using epidemic-broadcast-trees (EBT).
#[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))]
pub enable_ebt: bool,
/// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data
/// from peer; user beware - caveats about).
pub promisc: bool,
/// Disable the UNIX socket RPC interface.
pub nounixsock: bool,
}
// 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 {
database_directory: "~/.local/share/tildefriends".to_string(),
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
hmac: "".to_string(),
replication_hops: 1,
ip: "".to_string(),
ssb_port: "8008".to_string(),
localadv: false,
localdiscov: false,
enable_ebt: false,
promisc: false,
nounixsock: false,
}
}
}
impl 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 solar-sbot config.toml
let config_path = format!(
"{}/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)?;
let config: SbotConfig = toml::from_str(&config_contents)?;
Ok(config)
}
/// 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 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 solar-sbot config.toml
let config_path = format!(
"{}/tilde-sbot.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
// open config file for writing
let mut file = File::create(config_path)?;
// write the repo comment to file
write!(file, "{}", repo_comment)?;
// write the config string to file
write!(file, "{}", config_string)?;
Ok(())
}
}
/// Initialise an sbot client
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");
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

@ -1,111 +0,0 @@
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
pub fn is_sbot_online() -> Result<bool, PeachError> {
let output = Command::new("/usr/bin/systemctl")
.arg("status")
.arg("peach-go-sbot")
.output()?;
let status = output.status;
// returns true if the service had an exist status of 0 (is running)
let is_running = status.success();
Ok(is_running)
}
/// currently go-sbotcli determines where the working directory is
/// using the home directory of th user that invokes it
/// this could be changed to be supplied as CLI arg
/// but for now all sbotcli commands must first become peach-go-sbot before running
/// the sudoers file is configured to allow this to happen without a password
pub fn sbotcli_command() -> Command {
let mut command = Command::new("sudo");
command
.arg("-u")
.arg("peach-go-sbot")
.arg("/usr/bin/sbotcli");
command
}
pub fn post(msg: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command.arg("publish").arg("post").arg(msg).output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error making ssb post: {}", stderr),
})
}
}
#[derive(Serialize, Deserialize)]
struct WhoAmIValue {
id: String,
}
pub fn whoami() -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command.arg("call").arg("whoami").output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let value: WhoAmIValue = serde_json::from_str(text_output)?;
let id = value.id;
Ok(id)
}
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("invite")
.arg("create")
.arg("--uses")
.arg(uses.to_string())
.output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let output = text_output.replace("\n", "");
Ok(output)
}
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
let pub_ssb_id = whoami()?;
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("about")
.arg("--name")
.arg(new_name)
.arg(pub_ssb_id)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error updating pub name: {}", stderr),
})
}
}
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("post")
.arg("--recps")
.arg(recipient)
.arg(msg)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error sending ssb private message: {}", stderr),
})
}
}

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.1"
version = "0.5.0"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021"
description = "Query and configure network interfaces."

View File

@ -1,6 +1,6 @@
# peach-network
![Generic badge](https://img.shields.io/badge/version-0.4.0-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.4.2-<COLOR>.svg)
Network interface state query and modification library.

View File

@ -6,7 +6,7 @@ use std::num::ParseIntError;
use io::Error as IoError;
use probes::ProbeError;
use regex::Error as RegexError;
use wpactrl::WpaError;
use wpactrl::Error as WpaError;
/// Custom error type encapsulating all possible errors when querying
/// network interfaces and modifying their state.

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},
@ -22,6 +23,7 @@ use std::{
};
use probes::network;
use wpactrl::Client as WpaClient;
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
@ -105,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.
///
@ -121,7 +201,7 @@ pub struct Traffic {
/// In the event of an error, a `NetworkError` is returned in the `Result`.
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("SCAN")?;
let networks = wpa.request("SCAN_RESULTS")?;
let mut scan = Vec::new();
@ -138,7 +218,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
// we only want to return the auth / crypto flags
if flags_vec[0] != "[ESS]" {
// parse auth / crypto flag and assign it to protocol
protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str());
protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str());
}
let ssid = v[4].to_string();
let response = Scan {
@ -173,7 +253,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let networks = wpa.request("LIST_NETWORKS")?;
let mut id = Vec::new();
for network in networks.lines() {
@ -232,7 +312,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
/// `Result`.
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
@ -259,7 +339,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
/// the `Result`.
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
@ -291,7 +371,7 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`.
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
let mut wpa = WpaClient::builder().open()?;
let networks = wpa.request("LIST_NETWORKS")?;
let mut ssids = Vec::new();
for network in networks.lines() {
@ -323,7 +403,7 @@ pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
/// returned in the `Result`.
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder
@ -379,7 +459,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
/// a `NetworkError` is returned in the `Result`.
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let wpa_status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder
@ -579,7 +659,7 @@ pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError>
/// is returned in the `Result`.
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let select = format!("SELECT {}", id);
wpa.request(&select)?;
Ok(())
@ -598,7 +678,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
/// returned in the `Result`.
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let remove = format!("REMOVE_NETWORK {}", id);
wpa.request(&remove)?;
Ok(())
@ -617,7 +697,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
/// `Result`.
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let disable = format!("DISABLE_NETWORK {}", id);
wpa.request(&disable)?;
Ok(())
@ -634,7 +714,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
/// error, a `NetworkError` is returned in the `Result`.
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let disconnect = "DISCONNECT".to_string();
wpa.request(&disconnect)?;
Ok(())
@ -685,7 +765,7 @@ pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
wpa.request(&new_pass)?;
Ok(())
@ -702,7 +782,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
/// error, a `NetworkError` is returned in the `Result`.
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("REASSOCIATE")?;
Ok(())
}
@ -714,7 +794,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
/// `Result` type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`.
pub fn reconfigure() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
let mut wpa = WpaClient::builder().open()?;
wpa.request("RECONFIGURE")?;
Ok(())
}
@ -730,7 +810,7 @@ pub fn reconfigure() -> Result<(), NetworkError> {
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("DISCONNECT")?;
wpa.request("RECONNECT")?;
Ok(())
@ -742,7 +822,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn save() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
let mut wpa = WpaClient::builder().open()?;
wpa.request("SAVE_CONFIG")?;
Ok(())
}

View File

@ -1,6 +1,6 @@
[package]
name = "peach-stats"
version = "0.2.0"
version = "0.3.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."

View File

@ -1,10 +1,10 @@
# peach-stats
![Generic badge](https://img.shields.io/badge/version-0.2.0-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.3.0-<COLOR>.svg)
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
Currently offers the following statistics and associated data structures:
Currently offers the following system statistics and associated data structures:
- CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
- Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
@ -13,10 +13,14 @@ Currently offers the following statistics and associated data structures:
- Memory: `total`, `free`, `used`
- Uptime: `seconds`
As well as the following go-sbot process statistics:
- Sbot: `state`, `memory`, `uptime`, `downtime`
## Example Usage
```rust
use peach_stats::{stats, StatsError};
use peach_stats::{sbot, stats, StatsError};
fn main() -> Result<(), StatsError> {
let cpu = stats::cpu_stats()?;
@ -25,6 +29,7 @@ fn main() -> Result<(), StatsError> {
let load = stats::load_average()?;
let mem = stats::mem_stats()?;
let uptime = stats::uptime()?;
let sbot_process = sbot::sbot_stats()?;
// do things with the retrieved values...

View File

@ -1,7 +1,7 @@
//! Custom error type for `peach-stats`.
use probes::ProbeError;
use std::{error, fmt, io::Error as IoError};
use std::{error, fmt, io::Error as IoError, str::Utf8Error};
/// Custom error type encapsulating all possible errors when retrieving system
/// statistics.
@ -17,6 +17,10 @@ pub enum StatsError {
MemStat(ProbeError),
/// Failed to retrieve system uptime.
Uptime(IoError),
/// Systemctl command returned an error.
Systemctl(IoError),
/// Failed to interpret sequence of `u8` as a string.
Utf8String(Utf8Error),
}
impl error::Error for StatsError {}
@ -39,6 +43,12 @@ impl fmt::Display for StatsError {
StatsError::Uptime(ref source) => {
write!(f, "Failed to retrieve system uptime: {}", source)
}
StatsError::Systemctl(ref source) => {
write!(f, "Systemctl command returned an error: {}", source)
}
StatsError::Utf8String(ref source) => {
write!(f, "Failed to convert stdout to string: {}", source)
}
}
}
}

View File

@ -43,6 +43,7 @@
//! ```
pub mod error;
pub mod sbot;
pub mod stats;
pub use crate::error::StatsError;

111
peach-stats/src/sbot.rs Normal file
View File

@ -0,0 +1,111 @@
//! Systemd go-sbot process statistics retrieval functions and associated data types.
use std::{process::Command, str};
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use crate::StatsError;
/// go-sbot process statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct SbotStat {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
}
impl SbotStat {
/// Default builder for `SbotStat`.
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
}
}
}
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
let mut status = SbotStat::default();
let info_output = Command::new("sudo")
.arg("systemctl")
.arg("show")
.arg("go-sbot.service")
.arg("--no-page")
.output()
.map_err(StatsError::Systemctl)?;
let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
let status_output = Command::new("sudo")
.arg("systemctl")
.arg("status")
.arg("go-sbot.service")
.output()
.map_err(StatsError::Systemctl)?;
let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?;
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
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
Ok(status)
}

View File

@ -1,15 +0,0 @@
[package]
name = "peach-web-lite"
version = "0.1.0"
edition = "2021"
[dependencies]
env_logger = "0.9.0"
lazy_static = "1.4.0"
log = "0.4.14"
maud = "0.23.0"
peach-lib = { path = "../peach-lib" }
peach-network = { path = "../peach-network" }
peach-stats = { path = "../peach-stats" }
rouille = "3.5.0"
golgi = { path = "/home/glyph/Projects/playground/rust/golgi" }

View File

@ -1,24 +0,0 @@
# peach-web (lite)
A web interface for managing a Scuttlebutt pub.
## Application Structure
The application is divided between the route handlers (`src/routes`), context-builders (`src/context`), templates (`src/templates`) and authentication helper functions. The web server itself is initialised in `src/main.rs`. The context-builders are responsible for retrieving data which is required by the templates. This allows separation of data retrieval and data representation (the job of the templates).
## Built With
[Rouille](https://crates.io/crates/rouille): "a Rust web micro-framework".
[maud](https://crates.io/crates/maud): "Compile-time HTML templates".
[golgi](https://git.coopcloud.tech/golgi-ssb/golgi): "an experimental Scuttlebutt client library".
[peach-lib](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-lib).
[peach-network](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network).
[peach-stats](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats)
## Design Goals
Be lean and fast.
## Licensing
AGPL-3.0

View File

@ -1,68 +0,0 @@
{%- if ap_state == "up" %}
<!-- NETWORK CARD -->
<div class="card center">
<!-- NETWORK INFO BOX -->
<div class="capsule capsule-container success-border">
<!-- NETWORK STATUS GRID -->
<div class="two-grid" title="PeachCloud network mode and status">
<!-- 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">
</a>
<!-- left column -->
<!-- network mode icon with label -->
<div class="grid-column-1">
<img id="netModeIcon" class="center icon icon-active" src="/icons/router.svg" alt="WiFi router">
<label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online">ONLINE</label>
</div>
<!-- right column -->
<!-- network mode, ssid & ip with labels -->
<div class="grid-column-2">
<label class="label-small font-gray" for="netMode" title="Network Mode">MODE</label>
<p id="netMode" class="card-text" title="Network Mode">Access Point</p>
<label class="label-small font-gray" for="netSsid" title="Access Point SSID">SSID</label>
<p id="netSsid" class="card-text" title="SSID">peach</p>
<label class="label-small font-gray" for="netIp" title="Access Point IP Address">IP</label>
<p id="netIp" class="card-text" title="IP">{{ ap_ip }}</p>
</div>
</div>
<!-- horizontal dividing line -->
<hr>
<!-- DEVICES AND TRAFFIC GRID -->
<div class="three-grid card-container">
<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>
</div>
<label class="label-small font-gray">DEVICES</label>
</div>
<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 ap_traffic -%}
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in {{ ap_traffic.rx_unit }}">{{ ap_traffic.received }}</label>
<label class="label-small font-near-black">{{ ap_traffic.rx_unit }}</label>
{%- else -%}
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total"></label>
<label class="label-small font-near-black"></label>
{%- endif -%}
</div>
<label class="label-small font-gray">DOWNLOAD</label>
</div>
<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 ap_traffic -%}
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in {{ ap_traffic.tx_unit }}">{{ ap_traffic.transmitted }}</label>
<label class="label-small font-near-black">{{ ap_traffic.tx_unit }}</label>
{%- else -%}
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total"></label>
<label class="label-small font-near-black"></label>
{%- endif -%}
</div>
<label class="label-small font-gray">UPLOAD</label>
</div>
</div>
</div>
</div>

View File

@ -1,12 +0,0 @@
[ peach-web-lite ]
230 total dependencies
Finished release [optimized] target(s) in 35.12s
[ peach-web ]
522 total dependencies
Finished release [optimized] target(s) in 1m 33s

View File

@ -1,23 +0,0 @@
use log::debug;
use peach_lib::password_utils;
use crate::error::PeachWebError;
/// Password save form request handler. This function is for use by a user who is already logged in to change their password.
pub fn save_password_form(
current_password: String,
new_password1: String,
new_password2: String,
) -> Result<(), PeachWebError> {
debug!("attempting to change password");
password_utils::verify_password(&current_password)?;
// if the previous line did not throw an error, then the old password is correct
password_utils::validate_new_passwords(&new_password1, &new_password2)?;
// if the previous line did not throw an error, then the new password is valid
password_utils::set_new_password(&new_password1)?;
Ok(())
}

View File

@ -1,8 +0,0 @@
use peach_lib::config_manager;
use crate::error::PeachWebError;
pub fn ssb_admin_ids() -> Result<Vec<String>, PeachWebError> {
let peach_config = config_manager::load_peach_config()?;
Ok(peach_config.ssb_admin_ids)
}

View File

@ -1,4 +0,0 @@
pub mod admin;
pub mod network;
pub mod status;
pub mod test;

View File

@ -1,402 +0,0 @@
//! Data retrieval for the purpose of hydrating HTML templates.
use std::collections::HashMap;
use log::info;
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
};
use peach_network::{
network,
network::{Scan, Status, Traffic},
};
use crate::error::PeachWebError;
#[derive(Debug)]
pub struct AccessPoint {
pub detail: Option<Scan>,
pub signal: Option<i32>,
pub state: String,
}
pub fn ap_state() -> String {
match network::state("ap0") {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
}
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
// modify traffic values & assign measurement unit
// based on received and transmitted values
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
// convert to GB
(traffic.received / 1_073_741_824, "GB".to_string())
} else if traffic.received > 0 {
// otherwise, convert it to MB
((traffic.received / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
// convert to GB
(traffic.transmitted / 1_073_741_824, "GB".to_string())
} else if traffic.transmitted > 0 {
((traffic.transmitted / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
Some(IfaceTraffic {
rx,
rx_unit,
tx,
tx_unit,
})
}
#[derive(Debug)]
pub struct ConfigureDNSContext {
pub external_domain: String,
pub dyndns_subdomain: String,
pub enable_dyndns: bool,
pub is_dyndns_online: bool,
}
impl ConfigureDNSContext {
pub fn build() -> ConfigureDNSContext {
let peach_config = config_manager::load_peach_config().unwrap();
let dyndns_fulldomain = peach_config.dyn_domain;
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
let dyndns_subdomain =
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
ConfigureDNSContext {
external_domain: peach_config.external_domain,
dyndns_subdomain,
enable_dyndns: peach_config.dyn_enabled,
is_dyndns_online,
}
}
}
// TODO: this should probably rather go into the appropriate `routes` file
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)?;
// if dynamic dns is enabled and this is a new domain name, then register it
if enable_dyndns {
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
// check if this is a new domain or if its already registered
let is_new_domain = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain);
if is_new_domain {
match dyndns_client::register_domain(&full_dynamic_domain) {
Ok(_) => {
info!("Registered new dyndns domain");
// successful update
Ok(())
}
Err(err) => {
info!("Failed to register dyndns domain: {:?}", err);
// json response for failed update
let msg: String = match err {
// TODO: make this a nest high-level match
// PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(err), _))
PeachError::JsonRpcClientCore(source) => {
match source {
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
ErrorCode::ServerError(-32030) => {
format!("Error registering domain: {} was previously registered", full_dynamic_domain)
}
_ => {
format!("Failed to register dyndns domain {:?}", err)
}
},
_ => {
format!("Failed to register dyndns domain: {:?}", source)
}
}
}
_ => "Failed to register dyndns domain".to_string(),
};
Err(PeachWebError::FailedToRegisterDynDomain(msg))
}
}
}
// if the domain is already registered, then dont re-register, and just return success
else {
Ok(())
}
} else {
Ok(())
}
}
#[derive(Debug)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: String,
pub tx: u64,
pub tx_unit: String,
}
#[derive(Debug)]
pub struct NetworkDetailContext {
pub saved_aps: Vec<String>,
pub wlan_ip: String,
pub wlan_networks: HashMap<String, AccessPoint>,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
// TODO: read this value from the config file
let wlan_iface = "wlan0".to_string();
let wlan_ip = match network::ip(&wlan_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
let saved_aps = wlan_list.clone();
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
Ok(rssi) => rssi,
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&wlan_iface) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&wlan_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&wlan_iface) {
Ok(status) => status,
// interface unavailable
_ => None,
};
let wlan_traffic = match network::traffic(&wlan_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
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 {
detail: Some(ap),
state: "Available".to_string(),
signal: Some(quality_percent),
};
wlan_networks.insert(ssid, ap_detail);
}
for network in wlan_list {
// avoid repetition by checking that ssid is not already in list
if !wlan_networks.contains_key(&network) {
let ssid = network.clone();
let net_detail = AccessPoint {
detail: None,
state: "Not in range".to_string(),
signal: None,
};
wlan_networks.insert(ssid, net_detail);
}
}
NetworkDetailContext {
saved_aps,
wlan_ip,
wlan_networks,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}
#[derive(Debug)]
pub struct NetworkListContext {
pub ap_state: String,
pub wlan_networks: HashMap<String, String>,
pub wlan_ssid: String,
}
impl NetworkListContext {
pub fn build() -> NetworkListContext {
// TODO: read these values from the config file
let ap_iface = "ap0".to_string();
let wlan_iface = "wlan0".to_string();
//let wlan_iface = "wlp0s20f0u2".to_string();
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&wlan_iface) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
wlan_networks.insert(ap.ssid, "Available".to_string());
}
for network in wlan_list {
// insert ssid (with state) only if it doesn't already exist
wlan_networks
.entry(network)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network::state(&ap_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
NetworkListContext {
ap_state,
wlan_networks,
wlan_ssid,
}
}
}
#[derive(Debug)]
pub struct NetworkStatusContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkStatusContext {
pub fn build() -> Self {
// TODO: read these values from config file
let ap_iface = "ap0".to_string();
let wlan_iface = "wlan0".to_string();
let ap_ip = match network::ip(&ap_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let ap_ssid = match network::ssid(&ap_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not currently activated".to_string(),
};
let ap_state = match network::state(&ap_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let ap_traffic = match network::traffic(&ap_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
let wlan_ip = match network::ip(&wlan_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
Ok(rssi) => rssi,
_ => None,
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&wlan_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&wlan_iface) {
Ok(status) => status,
_ => None,
};
let wlan_traffic = match network::traffic(&wlan_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
NetworkStatusContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}

View File

@ -1,76 +0,0 @@
use peach_stats::{stats, stats::LoadAverage};
/// System statistics data.
pub struct StatusContext {
pub cpu_usage_percent: Option<f32>,
pub disk_usage_percent: Option<u32>,
pub disk_free: Option<u64>,
pub load_average: Option<LoadAverage>,
pub mem_usage_percent: Option<u64>,
pub mem_used: Option<u64>,
pub mem_free: Option<u64>,
pub mem_total: Option<u64>,
pub uptime: Option<u32>,
}
impl StatusContext {
pub fn build() -> StatusContext {
// convert result to Option<CpuStatPercentages>, discard any error
let cpu_usage_percent = stats::cpu_stats_percent()
.ok()
.map(|cpu| (cpu.nice + cpu.system + cpu.user).round());
let load_average = stats::load_average().ok();
let mem_stats = stats::mem_stats().ok();
let (mem_usage_percent, mem_used, mem_free, mem_total) = match mem_stats {
Some(mem) => (
Some((mem.used / mem.total) * 100),
Some(mem.used / 1024),
Some(mem.free / 1024),
Some(mem.total / 1024),
),
None => (None, None, None, None),
};
let uptime = match stats::uptime() {
Ok(secs) => {
let uptime_mins = secs / 60;
uptime_mins.to_string()
}
Err(_) => "Unavailable".to_string(),
};
// parse the uptime string to a signed integer (for math)
let uptime_parsed = uptime.parse::<u32>().ok();
let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => disks,
Err(_) => Vec::new(),
};
// select only the partition we're interested in: "/"
let disk_stats = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
let (disk_usage_percent, disk_free) = match disk_stats {
Some(disk) => (
Some(disk.used_percentage),
// calculate free disk space in megabytes
Some(disk.one_k_blocks_free / 1024),
),
None => (None, None),
};
StatusContext {
cpu_usage_percent,
disk_usage_percent,
disk_free,
load_average,
mem_usage_percent,
mem_used,
mem_free,
mem_total,
uptime: uptime_parsed,
}
}
}

View File

@ -1,12 +0,0 @@
use golgi;
use golgi::sbot::Sbot;
pub async fn test_async() -> Result<(), Box<dyn std::error::Error>> {
let mut sbot_client = Sbot::init(Some("127.0.0.1:8009".to_string()), None).await?;
let id = sbot_client.whoami().await?;
println!("whoami: {}", id);
Ok(())
}

View File

@ -1,60 +0,0 @@
//! Custom error type representing all possible error variants for peach-web.
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;
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
Json(JsonError),
Yaml(YamlError),
FailedToRegisterDynDomain(String),
PeachLib { source: PeachError, msg: String },
}
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::Json(ref source) => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
}
}
}
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
}
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLib {
source: err,
msg: "".to_string(),
}
}
}

View File

@ -1,42 +0,0 @@
use std::env;
use lazy_static::lazy_static;
use log::info;
mod auth;
mod context;
mod error;
mod router;
mod routes;
mod templates;
lazy_static! {
// determine run-mode from env var; default to standalone mode (aka peachpub)
static ref STANDALONE_MODE: bool = match env::var("PEACH_STANDALONE_MODE") {
// parse the value to a boolean; default to true for any error
Ok(val) => val.parse().unwrap_or(true),
Err(_) => true
};
}
fn main() {
env_logger::init();
rouille::start_server("localhost:8000", move |request| {
info!("Now listening on localhost:8000");
// static file server: matches on assets in the `static` directory
if let Some(request) = request.remove_prefix("/static") {
return rouille::match_assets(&request, "static");
}
// configure the router based on run-mode
if *STANDALONE_MODE {
info!("Running in standalone mode");
router::minimal_router(request)
} else {
info!("Running in fully-featured mode");
router::complete_router(request)
}
});
}

View File

@ -1,194 +0,0 @@
use rouille::{router, Request, Response};
use crate::routes;
/// Define router for standalone mode (PeachPub).
pub fn minimal_router(request: &Request) -> Response {
router!(request,
(GET) (/) => {
routes::home::menu()
},
(GET) (/help) => {
routes::help::menu()
},
(GET) (/login) => {
routes::login::login()
},
(POST) (/login) => {
routes::login::login_post(request)
},
(GET) (/scuttlebutt/blocks) => {
routes::scuttlebutt::blocks()
},
(GET) (/scuttlebutt/follows) => {
routes::scuttlebutt::follows()
},
(GET) (/scuttlebutt/followers) => {
routes::scuttlebutt::followers()
},
(GET) (/scuttlebutt/friends) => {
routes::scuttlebutt::friends()
},
(GET) (/scuttlebutt/peers) => {
routes::scuttlebutt::peers()
},
(GET) (/scuttlebutt/private) => {
routes::scuttlebutt::private()
},
(GET) (/scuttlebutt/profile) => {
routes::scuttlebutt::profile()
},
(GET) (/settings) => {
routes::settings::menu()
},
(GET) (/settings/admin) => {
routes::settings::admin()
},
(GET) (/settings/admin/configure) => {
routes::settings::admin_configure()
},
(GET) (/settings/admin/add) => {
routes::settings::admin_add()
},
(POST) (/settings/admin/add) => {
routes::settings::admin_add_post(request)
},
(GET) (/settings/admin/change_password) => {
routes::settings::admin_change_password()
},
(POST) (/settings/admin/change_password) => {
routes::settings::admin_change_password_post(request)
},
(POST) (/settings/admin/delete) => {
routes::settings::admin_delete_post(request)
},
(GET) (/settings/scuttlebutt) => {
routes::settings::scuttlebutt()
},
(GET) (/status) => {
routes::status::status()
},
// return 404 if not match is found
_ => routes::catchers::not_found()
)
}
/// Define router for fully-featured mode (PeachCloud).
pub fn complete_router(request: &Request) -> Response {
router!(request,
(GET) (/async) => {
routes::home::async_test()
},
(GET) (/) => {
routes::home::menu()
},
(GET) (/help) => {
routes::help::menu()
},
(GET) (/login) => {
routes::login::login()
},
(POST) (/login) => {
routes::login::login_post(request)
},
(GET) (/scuttlebutt/blocks) => {
routes::scuttlebutt::blocks()
},
(GET) (/scuttlebutt/follows) => {
routes::scuttlebutt::follows()
},
(GET) (/scuttlebutt/followers) => {
routes::scuttlebutt::followers()
},
(GET) (/scuttlebutt/friends) => {
routes::scuttlebutt::friends()
},
(GET) (/scuttlebutt/peers) => {
routes::scuttlebutt::peers()
},
(GET) (/scuttlebutt/private) => {
routes::scuttlebutt::private()
},
(GET) (/scuttlebutt/profile) => {
routes::scuttlebutt::profile()
},
(GET) (/settings) => {
routes::settings::menu()
},
(GET) (/settings/admin) => {
routes::settings::admin()
},
(GET) (/settings/admin/configure) => {
routes::settings::admin_configure()
},
(GET) (/settings/admin/add) => {
routes::settings::admin_add()
},
(POST) (/settings/admin/add) => {
routes::settings::admin_add_post(request)
},
(GET) (/settings/admin/change_password) => {
routes::settings::admin_change_password()
},
(POST) (/settings/admin/change_password) => {
routes::settings::admin_change_password_post(request)
},
(POST) (/settings/admin/delete) => {
routes::settings::admin_delete_post(request)
},
(GET) (/settings/network) => {
routes::settings::network()
},
(GET) (/settings/network/wifi) => {
routes::settings::network_list_aps()
},
(POST) (/settings/network/wifi/connect) => {
routes::settings::network_connect_wifi(request)
},
(POST) (/settings/network/wifi/disconnect) => {
routes::settings::network_disconnect_wifi(request)
},
(POST) (/settings/network/wifi/forget) => {
routes::settings::network_forget_wifi(request)
},
(GET) (/settings/network/wifi/ssid/{ssid: String}) => {
routes::settings::network_detail(ssid)
},
(GET) (/settings/network/wifi/add) => {
routes::settings::network_add_ap(None)
},
(GET) (/settings/network/wifi/add/{ssid: String}) => {
routes::settings::network_add_ap(Some(ssid))
},
(POST) (/settings/network/wifi/add) => {
routes::settings::network_add_ap_post(request)
},
(GET) (/settings/network/wifi/modify) => {
routes::settings::network_modify_ap(None)
},
// TODO: see if we can use the ?= syntax for ssid param
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
routes::settings::network_modify_ap(Some(ssid))
},
(POST) (/settings/network/wifi/modify) => {
routes::settings::network_modify_ap_post(request)
},
(GET) (/settings/network/dns) => {
routes::settings::network_configure_dns()
},
(POST) (/settings/network/dns) => {
routes::settings::network_configure_dns_post(request)
},
(GET) (/settings/scuttlebutt) => {
routes::settings::scuttlebutt()
},
(GET) (/status) => {
routes::status::status()
},
(GET) (/status/network) => {
routes::status::network()
},
// return 404 if not match is found
_ => routes::catchers::not_found()
)
}

View File

@ -1,10 +0,0 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn not_found() -> Response {
debug!("received GET request for a route which is not defined");
Response::html(templates::catchers::not_found()).with_status_code(404)
}

View File

@ -1,10 +0,0 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /help");
Response::html(templates::help::menu())
}

View File

@ -1,14 +0,0 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /");
Response::html(templates::home::menu())
}
pub fn async_test() -> Response {
Response::html(templates::home::async_test())
}

View File

@ -1,26 +0,0 @@
use log::debug;
use rouille::{post_input, try_or_400, Request, Response};
use crate::templates;
pub fn login() -> Response {
debug!("received GET request for: /login");
Response::html(templates::login::login())
}
pub fn login_post(request: &Request) -> Response {
debug!("received POST request for: /login");
let data = try_or_400!(post_input!(request, {
username: String,
password: String,
}));
// TODO: handle authentication...
debug!("{:?}", data);
// TODO: add flash message
Response::redirect_302("/")
}

View File

@ -1,7 +0,0 @@
pub mod catchers;
pub mod help;
pub mod home;
pub mod login;
pub mod scuttlebutt;
pub mod settings;
pub mod status;

View File

@ -1,46 +0,0 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn blocks() -> Response {
debug!("received GET request for: /scuttlebutt/blocks");
Response::html(templates::scuttlebutt::peers_list("Blocks".to_string()))
}
pub fn follows() -> Response {
debug!("received GET request for: /scuttlebutt/follows");
Response::html(templates::scuttlebutt::peers_list("Follows".to_string()))
}
pub fn followers() -> Response {
debug!("received GET request for: /scuttlebutt/followers");
Response::html(templates::scuttlebutt::peers_list("Followers".to_string()))
}
pub fn friends() -> Response {
debug!("received GET request for: /scuttlebutt/friends");
Response::html(templates::scuttlebutt::peers_list("Friends".to_string()))
}
pub fn peers() -> Response {
debug!("received GET request for: /scuttlebutt/peers");
Response::html(templates::scuttlebutt::peers())
}
pub fn private() -> Response {
debug!("received GET request for: /scuttlebutt/private");
Response::html(templates::scuttlebutt::private())
}
pub fn profile() -> Response {
debug!("received GET request for: /scuttlebutt/profile");
Response::html(templates::scuttlebutt::profile(None, None))
}

View File

@ -1,340 +0,0 @@
use log::{debug, warn};
use peach_lib::config_manager;
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::auth;
use crate::context;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /settings");
Response::html(templates::settings::menu::menu())
}
pub fn admin() -> Response {
debug!("received GET request for: /settings/admin");
Response::html(templates::settings::admin::menu())
}
pub fn admin_configure() -> Response {
debug!("received GET request for: /settings/admin/configure");
Response::html(templates::settings::admin::configure(None, None))
}
pub fn admin_add() -> Response {
debug!("received GET request for: /settings/admin/add");
Response::html(templates::settings::admin::add(None, None))
}
pub fn admin_add_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/add");
let data = try_or_400!(post_input!(request, { ssb_id: String }));
debug!("{:?}", data);
let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
"success".to_string(),
"Added new SSB administrator ID".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to add new SSB administrator ID: {}", e),
),
};
Response::html(templates::settings::admin::add(
Some(flash_msg),
Some(flash_name),
))
}
pub fn admin_change_password() -> Response {
debug!("received GET request for: /settings/admin/change_password");
Response::html(templates::settings::admin::change_password(None, None))
}
pub fn admin_change_password_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/change_password");
let data = try_or_400!(post_input!(request, {
current_password: String,
new_password1: String,
new_password2: String
}));
// attempt to update the password
let (flash_name, flash_msg) = match auth::save_password_form(
data.current_password,
data.new_password1,
data.new_password2,
) {
Ok(_) => ("success".to_string(), "Saved new password".to_string()),
Err(e) => (
"error".to_string(),
format!("Failed to save new password: {}", e),
),
};
Response::html(templates::settings::admin::change_password(
Some(flash_msg),
Some(flash_name),
))
}
pub fn admin_delete_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/delete");
let data = try_or_400!(post_input!(request, { ssb_id: String }));
let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
"success".to_string(),
"Removed SSB administrator ID".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to remove SSB administrator ID: {}", e),
),
};
Response::html(templates::settings::admin::configure(
Some(flash_msg),
Some(flash_name),
))
}
pub fn network() -> Response {
debug!("received GET request for: /settings/network");
Response::html(templates::settings::network::menu())
}
pub fn network_add_ap(ssid: Option<String>) -> Response {
debug!("received GET request for: /settings/network/wifi/add");
Response::html(templates::settings::network::add_ap(ssid, None, None))
}
pub fn network_add_ap_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/add");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
/* ADD WIFI CREDENTIALS FOR AP */
// check if the credentials already exist for this access point
let creds_exist = match network::saved_networks() {
Ok(Some(ssids)) => ssids.contains(&data.ssid),
_ => false,
};
let (flash_name, flash_msg) = if creds_exist {
(
"error".to_string(),
"Network credentials already exist for this access point".to_string(),
)
} else {
// if credentials not found, generate and write wifi config to wpa_supplicant
match network::add(&wlan_iface, &data.ssid, &data.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))
}
}
};
Response::html(templates::settings::network::add_ap(
None,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_configure_dns() -> Response {
debug!("received GET request for: /settings/network/dns");
Response::html(templates::settings::network::configure_dns(None, None))
}
pub fn network_configure_dns_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/dns");
let data = try_or_400!(
post_input!(request, { external_domain: String, enable_dyndns: bool, dynamic_domain: String })
);
let (flash_name, flash_msg) = match context::network::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(e) => (
"error".to_string(),
format!("Failed to save DNS configuration: {}", e),
),
};
Response::html(templates::settings::network::configure_dns(
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_modify_ap(ssid: Option<String>) -> Response {
debug!("received GET request for: /settings/network/wifi/modify");
Response::html(templates::settings::network::modify_ap(ssid, None, None))
}
pub fn network_modify_ap_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/modify");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
/* MODIFY WIFI CREDENTIALS FOR AP */
let (flash_name, flash_msg) = match network::update(&wlan_iface, &data.ssid, &data.pass) {
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
Err(e) => (
"error".to_string(),
format!("Failed to update WiFi password: {}", e),
),
};
Response::html(templates::settings::network::modify_ap(
None,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_connect_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/connect");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::id(&wlan_iface, &data.ssid) {
Ok(Some(id)) => match network::connect(&id, &wlan_iface) {
Ok(_) => (
"success".to_string(),
"Connected to chosen network".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to connect to chosen network: {}", e),
),
},
_ => (
"error".to_string(),
"Failed to retrieve the network ID".to_string(),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_disconnect_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/disconnect");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::disable(&wlan_iface, &data.ssid) {
Ok(_) => (
"success".to_string(),
"Disconnected from WiFi network".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to disconnect from WiFi network: {}", e),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_forget_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/forget");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::forget(&wlan_iface, &data.ssid) {
Ok(_) => (
"success".to_string(),
"WiFi credentials removed".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to remove WiFi credentials: {}", e),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_detail(ssid: String) -> Response {
debug!("received GET request for: /settings/network/wifi/<selected>");
Response::html(templates::settings::network::network_detail(
ssid, None, None,
))
}
pub fn network_list_aps() -> Response {
debug!("received GET request for: /settings/network/wifi");
Response::html(templates::settings::network::list_aps())
}
pub fn scuttlebutt() -> Response {
debug!("received GET request for: /settings/scuttlebutt");
Response::html(templates::settings::scuttlebutt::scuttlebutt())
}

View File

@ -1,16 +0,0 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn network() -> Response {
debug!("received GET request for: /status/network");
Response::html(templates::status::network())
}
pub fn status() -> Response {
debug!("received GET request for: /status");
Response::html(templates::status::status())
}

View File

@ -1,51 +0,0 @@
use maud::{html, PreEscaped, DOCTYPE};
pub fn base(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
html! {
(DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
title { "PeachCloud" }
meta name="description" content="PeachCloud Network";
meta name="author" content="glyph";
meta name="viewport" content="width=device-width, initial-scale=1.0";
link rel="icon" type="image/x-icon" href="/static/icons/peach-icon.png";
link rel="stylesheet" href="/static/css/peachcloud.css";
}
body {
// render the navigation template
(nav(back, title, content))
}
}
}
}
pub fn nav(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- TOP NAV BAR -->"))
nav class="nav-bar" {
a class="nav-item" href=(back) title="Back" {
img class="icon-medium nav-icon-left icon-active" src="/static/icons/back.svg" alt="Back";
}
h1 class="nav-title" { (title) }
a class="nav-item" id="logoutButton" href="/logout" title="Logout" {
img class="icon-medium nav-icon-right icon-active" src="/static/icons/enter.svg" alt="Enter";
}
}
(PreEscaped("<!-- MAIN CONTENT CONTAINER -->"))
main { (content) }
(PreEscaped("<!-- BOTTOM NAV BAR -->"))
nav class="nav-bar" {
a class="nav-item" href="https://scuttlebutt.nz/" {
img class="icon-medium nav-icon-left" title="Scuttlebutt Website" src="/static/icons/hermies.png" alt="Secure Scuttlebutt";
}
a class="nav-item" href="/" {
img class="icon nav-icon-left" src="/static/icons/peach-icon.png" alt="PeachCloud" title="Home";
}
a class="nav-item" href="/power" {
img class="icon-medium nav-icon-right icon-active" title="Shutdown" src="/static/icons/power.svg" alt="Power switch";
}
}
}
}

View File

@ -1,21 +0,0 @@
use maud::{html, PreEscaped};
use crate::templates;
pub fn not_found() -> PreEscaped<String> {
let back = "/".to_string();
let title = "404 Not Found".to_string();
let content = html! {
div class="card center" {
div class="capsule-container" {
div class="capsule info-border" {
p { "No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct." }
p { "Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home." }
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,19 +0,0 @@
use maud::{html, PreEscaped};
use crate::templates;
// /help
pub fn menu() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Help".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
p { "help content goes here" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,77 +0,0 @@
use maud::{html, PreEscaped};
use crate::context;
use crate::templates;
pub async fn async_test() -> PreEscaped<String> {
let back = "".to_string();
let title = "".to_string();
let whoami = context::test::test_async().await.unwrap();
let content = html! {
p { (whoami) }
};
templates::base::base(back, title, content)
}
pub fn menu() -> PreEscaped<String> {
let back = "".to_string();
let title = "".to_string();
let content = html! {
(PreEscaped("<!-- RADIAL MENU -->"))
div class="grid" {
(PreEscaped("<!-- top-left -->"))
(PreEscaped("<!-- PEERS LINK AND ICON -->"))
a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/users.svg";
}
}
(PreEscaped("<!-- top-middle -->"))
(PreEscaped("<!-- CURRENT USER LINK AND ICON -->"))
a class="top-middle" href="/scuttlebutt/profile" title="Profile" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/user.svg";
}
}
(PreEscaped("<!-- top-right -->"))
(PreEscaped("<!-- MESSAGES LINK AND ICON -->"))
a class="top-right" href="/scuttlebutt/private" title="Private Messages" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/envelope.svg";
}
}
(PreEscaped("<!-- middle -->"))
a class="middle" href="/hello" {
div class="circle circle-large" { }
}
(PreEscaped("<!-- bottom-left -->"))
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
a class="bottom-left" href="/status" title="Status" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/heart-pulse.svg";
}
}
(PreEscaped("<!-- bottom-middle -->"))
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
a class="bottom-middle" href="/help" title="Help Menu" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/book.svg";
}
}
(PreEscaped("<!-- bottom-right -->"))
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
a class="bottom-right" href="/settings" title="Settings Menu" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/cog.svg";
}
}
}
};
// we pass the content of this template into the base template
templates::base::base(back, title, content)
}

View File

@ -1,30 +0,0 @@
use maud::{html, PreEscaped};
use crate::templates;
// https://github.com/tomaka/rouille/blob/master/examples/login-session.rs
// /login
pub fn login() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Login".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
form id="login_form" action="/login" method="post" {
// input field for username
input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus { }
// input field for password
input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username" { }
div id="buttonDiv" {
// login button
input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login" { }
}
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,9 +0,0 @@
pub mod base;
pub mod catchers;
pub mod help;
pub mod home;
pub mod login;
pub mod scuttlebutt;
pub mod settings;
pub mod snippets;
pub mod status;

View File

@ -1,109 +0,0 @@
use maud::{html, PreEscaped};
use crate::templates;
// /scuttlebutt/peers
pub fn peers() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Scuttlebutt Peers".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT PEERS -->"))
div class="card center" {
div class="card-container" {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
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="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers" { "Followers" }
a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List Blocks" { "Blocks" }
}
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/friends
// /scuttlebutt/follows
// /scuttlebutt/followers
// /scuttlebutt/blocks
pub fn peers_list(title: String) -> PreEscaped<String> {
let back = "/scuttlebutt/peers".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT PEERS LIST -->"))
div class="card center" {
ul class="list" {
// for peer in peers
li {
a class="list-item link light-bg" href="/scuttlebutt/profile/(pub_key)" {
img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image";
p id="peerName" class="list-text" { "(name)" }
label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key" { "(public_key)" }
}
}
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/private
pub fn private() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Private Messages".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
p { "private message content goes here" }
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/profile
pub fn profile(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let back = "/".to_string();
let title = "Scuttlebutt Profile".to_string();
let content = html! {
(PreEscaped("<!-- USER PROFILE -->"))
div class="card center" {
(PreEscaped("<!-- PROFILE INFO BOX -->"))
div class="capsule capsule-profile" title="Scuttlebutt account profile information" {
(PreEscaped("<!-- edit profile button -->"))
img id="editProfile" class="icon-small nav-icon-right" src="/icons/pencil.svg" alt="Profile picture";
(PreEscaped("<!-- PROFILE BIO -->"))
(PreEscaped("<!-- profile picture -->"))
img id="profilePicture" class="icon-large" src="{ image_path }" alt="Profile picture";
(PreEscaped("<!-- name, public key & description -->"))
p id="profileName" class="card-text" title="Name" { "(name)" }
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" { "(public_key)" }
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" { "(description)" }
}
(PreEscaped("<!-- PUBLIC POST FORM -->"))
form id="postForm" action="/scuttlebutt/post" method="post" {
(PreEscaped("<!-- input for message contents -->"))
textarea id="publicPost" class="center input message-input" title="Compose Public Post" { }
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
}
(PreEscaped("<!-- BUTTONS -->"))
(PreEscaped("<!-- TODO: each of these buttons needs to be a form with a public key -->"))
div id="buttons" {
a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer" { "Follow" }
a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer" { "Block" }
a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message" { "Private Message" }
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}

View File

@ -1,122 +0,0 @@
use maud::{html, PreEscaped};
use crate::context::admin;
use crate::templates;
// /settings/admin
pub fn menu() -> PreEscaped<String> {
let back = "/settings".to_string();
let title = "Administrator Settings".to_string();
let content = html! {
(PreEscaped("<!-- ADMIN SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password" { "Change Password" }
a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" }
}
}
};
templates::base::base(back, title, content)
}
// /settings/admin/add
pub fn add(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let back = "/settings/admin/configure".to_string();
let title = "Add Administrator".to_string();
let content = html! {
(PreEscaped("<!-- ADD ADMIN FORM -->"))
div class="card center" {
div class="card-container" {
form id="addAdminForm" action="/settings/admin/add" method="post" {
input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value="";
div id="buttonDiv" {
input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add";
a class="button button-secondary center" href="/settings/admin/configure" title="Cancel" { "Cancel" }
}
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
// /settings/admin/configure
pub fn configure(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let ssb_admin_ids = admin::ssb_admin_ids();
let back = "/settings/admin".to_string();
let title = "Administrator Configuration".to_string();
let content = html! {
(PreEscaped("<!-- CONFIGURE ADMIN PAGE -->"))
div class="card center" {
div class="text-container" {
h4 { "Current Admins" }
@match ssb_admin_ids {
Ok(admins) => {
@if admins.is_empty() {
div { "No administators are currently configured" }
} else {
@for admin in admins {
div {
form action="/settings/admin/delete" method="post" {
input type="hidden" name="ssb_id" value="{{admin}}";
input type="submit" value="X" title="Delete" {
span { (admin) }
}
}
}
}
}
},
Err(e) => div { "Encountered an error while trying to retrieve list of administrators: " (e) }
}
a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin" { "Add Admin" }
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}
// /settings/admin/change_password
pub fn change_password(
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/admin".to_string();
let title = "Change Password".to_string();
let content = html! {
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
div class="card center" {
div class="form-container" {
form id="changePassword" action="/settings/admin/change_password" method="post" {
(PreEscaped("<!-- input for current password -->"))
input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus;
(PreEscaped("<!-- input for new password -->"))
input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password";
(PreEscaped("<!-- input for duplicate new password -->"))
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate";
div id="buttonDiv" {
input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save";
a class="button button-secondary center" href="/settings/admin" title="Cancel" { "Cancel" }
}
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,37 +0,0 @@
use maud::{html, PreEscaped};
use crate::{templates, STANDALONE_MODE};
// /settings
pub fn menu() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Settings".to_string();
// render a minimal menu (no network settings) if running in standalone mode
let content = if *STANDALONE_MODE {
html! {
(PreEscaped("<!-- SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
}
}
}
} else {
html! {
(PreEscaped("<!-- SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,4 +0,0 @@
pub mod admin;
pub mod menu;
pub mod network;
pub mod scuttlebutt;

View File

@ -1,319 +0,0 @@
use maud::{html, PreEscaped};
use crate::context::{
network,
network::{ConfigureDNSContext, NetworkDetailContext, NetworkListContext},
};
use crate::templates;
// /settings/network/wifi/add
pub fn add_ap(
ssid: Option<String>,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/network".to_string();
let title = "Add WiFi Network".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK ADD CREDENTIALS FORM -->"))
div class="card center" {
div class="card-container" {
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
(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=[ssid] autofocus;
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
div id="buttonDiv" {
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" }
}
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
/* TODO: I JUST OVERWROTE THE network_detail FUNCTION :'( :'( :'( */
// /settings/network/wifi
pub fn network_detail(
// the ssid of the network we wish to examine in detail
selected: String,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
// retrieve network detail data
let context = NetworkDetailContext::build();
// have credentials for the access point we're viewing previously been saved?
// ie. is this a known access point?
let selected_is_saved = context.saved_aps.contains(&selected);
let back = "/settings/network".to_string();
let title = "WiFi Network Detail".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK DETAIL -->"))
// select only the access point we are interested in
@match context.wlan_networks.get_key_value(&selected) {
Some((ssid, ap)) => {
@let capsule_class = if ssid == &context.wlan_ssid {
"two-grid capsule success-border"
} else {
"two-grid capsule"
};
@let ap_status = if ssid == &context.wlan_ssid {
"CONNECTED"
} else if ap.state == "Available" {
"AVAILABLE"
} else {
"NOT IN RANGE"
};
@let ap_protocol = match &ap.detail {
Some(scan) => scan.protocol.clone(),
None => "Unknown".to_string()
};
@let ap_signal = match ap.signal {
Some(signal) => signal.to_string(),
None => "Unknown".to_string()
};
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(capsule_class) title="PeachCloud network mode and status" {
(PreEscaped("<!-- left column -->"))
(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" { (ap_status) }
}
(PreEscaped("<!-- right column -->"))
(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) }
}
}
(PreEscaped("<!-- BUTTONS -->"))
div class="card-container" style="padding-top: 0;" {
div id="buttonDiv" {
@if context.wlan_ssid == selected {
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
// 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";
}
}
// If the selected access point appears in the list,
// display the Modify and Forget buttons.
@if context.saved_aps.contains(&selected) {
@if context.wlan_ssid != selected && ap.state == "Available" {
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
// 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";
}
}
a class="button button-primary center" href="/settings/network/wifi/modify/"{(ssid)}"" { "Modify" }
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
// 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";
}
}
@if !selected_is_saved {
// 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)}"" { "Add" };
}
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
}
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
}
// TODO: improve the styling of this
None => {
div class="card center" {
p { "Selected access point was not found in-range or in the list of saved access points" }
}
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/wifi
pub fn list_aps() -> PreEscaped<String> {
// retrieve network list data
let context = NetworkListContext::build();
let back = "/settings/network".to_string();
let title = "WiFi Networks".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK ACCESS POINT LIST -->"))
div class="card center" {
div class="center list-container" {
ul class="list" {
@if context.ap_state == *"up" {
li class="list-item light-bg warning-border" { "Enable WiFi client mode to view saved and available networks." }
} @else if !context.wlan_networks.is_empty() {
@for (ssid, state) in context.wlan_networks {
li {
@if ssid == context.wlan_ssid {
a class="list-item link primary-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi online";
p class="list-text" { (context.wlan_ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
}
} @else if state == "Available" {
a class="list-item link light-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
}
} @else {
a class="list-item link" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
}
}
}
}
}
li class="list-item light-bg" { "No saved or available networks found." }
}
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/wifi
pub fn modify_ap(
ssid: Option<String>,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/network".to_string();
let title = "Modify WiFi Network".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
div class="card center" {
div class="card-container" {
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
(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=[ssid] autofocus;
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
div id="buttonDiv" {
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" }
}
}
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/dns
pub fn configure_dns(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
// retrieve dyndns-related data
let context = ConfigureDNSContext::build();
let back = "/settings/network".to_string();
let title = "Configure DNS".to_string();
let content = html! {
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
div class="card center" {
div class="form-container" {
@if context.enable_dyndns {
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
div id="dyndns-status-indicator" class="stack capsule{% if is_dyndns_online %} success-border{% else %} warning-border{% endif %}" {
div class="stack" {
@if context.is_dyndns_online {
label class="label-small font-near-black" { "Dynamic DNS is currently online." }
} else {
label class="label-small font-near-black" { "Dynamic DNS is enabled but may be offline." }
}
}
}
}
form id="configureDNS" action="/settings/network/dns" method="post" {
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=(context.external_domain);
}
}
div class="input-wrapper" {
div {
(PreEscaped("<!-- checkbox for dynds 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 dyndns" type="checkbox" { @if context.enable_dyndns { "checked" } };
}
}
div class="input-wrapper" {
(PreEscaped("<!-- input for dyndns -->"))
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=(context.dyndns_subdomain) { ".dyn.peachcloud.org" }
}
}
div id="buttonDiv" {
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
}
}
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}
// /settings/network
pub fn menu() -> PreEscaped<String> {
let ap_state = network::ap_state();
let back = "/settings".to_string();
let title = "Network Settings".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK SETTINGS CARD -->"))
div class="card center" {
(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 \"Deploy Access Point\" -->"))
@if ap_state == *"up" {
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
} @else {
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
}
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,29 +0,0 @@
use maud::{html, PreEscaped};
use crate::templates;
// /settings/scuttlebutt
pub fn scuttlebutt() -> PreEscaped<String> {
let back = "/settings".to_string();
let title = "Scuttlebutt Settings".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key" { "Set Network Key" }
a id="replicationHops" class="button button-primary center" href="/settings/scuttlebutt/hops" title="Set Replication Hops" { "Set Replication Hops" }
a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds" { "Remove Blocked Feeds" }
a id="setDirectory" class="button button-primary center" href="/settings/scuttlebutt/set_directory" title="Set Database Directory" { "Set Database Directory" }
a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem" { "Check Filesystem" }
a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem" { "Repair Filesystem" }
a id="disable" class="button button-primary center" href="/settings/scuttlebutt/disable" title="Disable Sbot" { "Disable Sbot" }
a id="enable" class="button button-primary center" href="/settings/scuttlebutt/enable" title="Enable Sbot" { "Enable Sbot" }
a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot" { "Restart Sbot" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,22 +0,0 @@
use maud::{html, PreEscaped};
pub fn flash_message(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- FLASH MESSAGE -->"))
@if flash_msg.is_some() {
@if flash_name == Some("success".to_string()) {
div class="capsule center-text flash-message font-success" {
(flash_msg.unwrap())
}
} @else if flash_name == Some("info".to_string()) {
div class="capsule center-text flash-message font-info" {
(flash_msg.unwrap())
}
} @else if flash_name == Some("error".to_string()) {
div class="capsule center-text flash-message font-failure" {
(flash_msg.unwrap())
}
}
}
}
}

View File

@ -1,496 +0,0 @@
use maud::{html, PreEscaped};
use crate::context::{network::NetworkStatusContext, status::StatusContext};
use crate::{templates, STANDALONE_MODE};
fn ap_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class="capsule capsule-container success-border" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div class="two-grid" title="PeachCloud network mode and status" {
(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="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/router.svg" alt="WiFi router";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online" { "ONLINE" }
}
(PreEscaped("<!-- right column -->"))
(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" { "Access Point" }
label class="label-small font-gray" for="netSsid" title="Access Point SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.ap_ssid) }
label class="label-small font-gray" for="netIp" title="Access Point IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.ap_ip) }
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- DEVICES AND TRAFFIC GRID -->"))
div class="three-grid card-container" {
// devices stack
div class="stack" {
img id="devices" class="icon icon-medium" title="Connected devices" src="/static/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" }
}
// download stack
div class="stack" {
img id="dataDownload" class="icon icon-medium" title="Download" src="/static/icons/down-arrow.svg" alt="Download";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.ap_traffic {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total";
label class="label-small font-near-black" { "0" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
// upload stack
div class="stack" {
img id="dataUpload" class="icon icon-medium" title="Upload" src="/static/icons/up-arrow.svg" alt="Upload";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.ap_traffic {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total";
label class="label-small font-near-black" { "0" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
}
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
let capsule = if status.wlan_state == *"up" {
"capsule capsule-container success-border"
} else {
"capsule capsule-container warning-border"
};
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(capsule) {
@if status.wlan_state == *"up" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- NETWORK STATUS -->"))
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
} @else {
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
(PreEscaped("<!-- row of icons representing network statistics -->"))
div class="three-grid card-container" {
div class="stack" {
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/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 (%)" {
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
}
}
label class="label-small font-gray" { "SIGNAL" }
}
div class="stack" {
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
div class="stack" {
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
}
/*
* WORKS ... kinda
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
@if status.wlan_state == *"up" {
div class="capsule capsule-container success-border" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- NETWORK STATUS -->"))
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
} @else {
div class="capsule capsule-container warning-border" {
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
(PreEscaped("<!-- row of icons representing network statistics -->"))
div class="three-grid card-container" {
div class="stack" {
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/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 (%)" {
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
}
}
label class="label-small font-gray" { "SIGNAL" }
}
div class="stack" {
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
div class="stack" {
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
*/
pub fn network() -> PreEscaped<String> {
let back = "/status".to_string();
let title = "Network Status".to_string();
// retrieve network status data
let status = NetworkStatusContext::build();
let content = html! {
(PreEscaped("<!-- NETWORK STATUS -->"))
// if ap is up, show ap card, else show wlan card
@if status.ap_state == *"up" {
(ap_network_card(status))
} @else {
(wlan_network_card(status))
}
};
templates::base::base(back, title, content)
}
fn scuttlebutt_status() -> PreEscaped<String> {
html! {
(PreEscaped("<!-- SCUTTLEBUTT STATUS -->"))
div class="card center" {
div class="card-container" {
p { "Network key: " }
p { "Replication hops: " }
p { "Sbot version: " }
p { "Process status: " }
p { "Process uptime: " }
p { "Blobstore size: " }
p { "Latest sequence number: " }
p { "Last time you visited this page, latest sequence was x ... now it's y" }
p { "Number of follows / followers / friends / blocks" }
}
}
}
}
pub fn status() -> PreEscaped<String> {
let back = "/".to_string();
let title = if *STANDALONE_MODE {
"Scuttlebutt Status".to_string()
} else {
"System Status".to_string()
};
// retrieve system status data
let status = StatusContext::build();
// render the scuttlebutt status template
let content = if *STANDALONE_MODE {
scuttlebutt_status()
// or render the complete system status template
} else {
html! {
(PreEscaped("<!-- SYSTEM STATUS -->"))
div class="card center" {
div class="card-container" {
div class="three-grid" {
(PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
(PreEscaped("<!-- Display microservice status for network, oled & stats -->"))
a class="link" href="/status/network" {
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Network" title="Network microservice status" src="/static/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" { "(network_ping)" };
}
}
}
(PreEscaped("<!-- PEACH-OLED STATUS STACK -->"))
div class="stack capsule success-border" {
img id="oledIcon" class="icon icon-medium" alt="Display" title="OLED display microservice status" src="/static/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" { "(oled_ping)" };
}
}
(PreEscaped("<!-- PEACH-STATS STATUS STACK -->"))
div class="stack capsule success-border" {
img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/static/icons/chart.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Statistics" };
label class="label-small font-near-black" { "AVAILABLE" };
}
}
(PreEscaped("<!-- DYNDNS STATUS STACK -->"))
(PreEscaped("<!-- Display status for dynsdns, config & sbot -->"))
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Dyndns" title="Dyndns status" src="/static/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" { "(dns_ping)" };
}
}
(PreEscaped("<!-- CONFIG STATUS STACK -->"))
// TODO: render capsule border according to status
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Config" title="Config status" src="/static/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" { "(status)" };
}
}
(PreEscaped("<!-- SBOT STATUS STACK -->"))
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Sbot" title="Sbot status" src="/static/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" { "(sbot_status)" }
}
}
}
}
div class="card-container" {
(PreEscaped("<!-- Display CPU usage meter -->"))
@match status.cpu_usage_percent {
Some(x) => {
div class="flex-grid" {
span class="card-text" { "CPU" }
span class="label-small push-right" { (x) "%" }
}
meter value=(x) min="0" max="100" title="CPU usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "CPU Usage" }
}
}
},
_ => p class="card-text" { "CPU usage data unavailable" }
}
(PreEscaped("<!-- Display memory usage meter -->"))
@match status.mem_usage_percent {
Some(x) => {
@let (mem_free, mem_unit) = match status.mem_free {
Some(x) => {
if x > 1024 {
((x / 1024), "GB".to_string())
} else {
(x, "MB".to_string())
}
},
_ => (0, "MB".to_string())
};
@let mem_total = status.mem_total.unwrap_or(0);
@let mem_used = status.mem_used.unwrap_or(0);
div class="flex-grid" {
span class="card-text" { "Memory" }
span class="label-small push-right" { (x) " % ("(mem_free)" "(mem_unit)" free)" }
}
meter value=(mem_used) min="0" max=(mem_total) title="Memory usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "Memory Usage" }
}
}
},
_ => p class="card-text" { "Memory usage data unavailable" }
}
(PreEscaped("<!-- Display disk usage meter -->"))
@match status.disk_usage_percent {
Some(x) => {
// define free disk space with appropriate unit (GB if > 1024)
@let (disk_free, disk_unit) = match status.disk_free {
Some(x) => {
if x > 1024 {
((x / 1024), "GB".to_string())
} else {
(x, "MB".to_string())
}
},
_ => (0, "MB".to_string())
};
div class="flex-grid" {
span class="card-text" { "Disk" };
span class="label-small push-right" { (x) " % ("(disk_free)" "(disk_unit)")" };
}
meter value=(x) min="0" max="100" title="Disk usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "Disk Usage" };
}
}
},
_ => p class="card-text" { "Disk usage data unavailable" }
}
(PreEscaped("<!-- Display system uptime in minutes -->"))
@match status.uptime {
Some(x) => {
@if x < 60 {
p class="capsule center-text" { "Uptime: "(x)" minutes" }
} @else {
@let hours = x / 60;
@let mins = x % 60;
p class="capsule center-text" { "Uptime: "(hours)" hours, "(mins)" minutes" }
}
},
_ => p class="card-text" { "Uptime data unavailable" }
}
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -1,177 +0,0 @@
/*
VARIABLES
*/
@custom-media --breakpoint-not-small screen and (min-width: 30em);
@custom-media --breakpoint-medium screen and (min-width: 30em) and (max-width: 60em);
@custom-media --breakpoint-large screen and (min-width: 60em);
:root {
--sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif;
--serif: georgia, serif;
--code: consolas, monaco, monospace;
--font-size-headline: 6rem;
--font-size-subheadline: 5rem;
--font-size-1: 3rem;
--font-size-2: 2.25rem;
--font-size-3: 1.5rem;
--font-size-4: 1.25rem;
--font-size-5: 1rem;
--font-size-6: .875rem;
--font-size-7: .75rem;
--letter-spacing-tight:-.05em;
--letter-spacing-1:.1em;
--letter-spacing-2:.25em;
--line-height-solid: 1;
--line-height-title: 1.25;
--line-height-copy: 1.5;
--measure: 30em;
--measure-narrow: 20em;
--measure-wide: 34em;
--spacing-none: 0;
--spacing-extra-small: .25rem;
--spacing-small: .5rem;
--spacing-medium: 1rem;
--spacing-large: 2rem;
--spacing-extra-large: 4rem;
--spacing-extra-extra-large: 8rem;
--spacing-extra-extra-extra-large: 16rem;
--spacing-copy-separator: 1.5em;
--height-1: 1rem;
--height-2: 2rem;
--height-3: 4rem;
--height-4: 8rem;
--height-5: 16rem;
--width-1: 1rem;
--width-2: 2rem;
--width-3: 4rem;
--width-4: 8rem;
--width-5: 16rem;
--max-width-1: 1rem;
--max-width-2: 2rem;
--max-width-3: 4rem;
--max-width-4: 8rem;
--max-width-5: 16rem;
--max-width-6: 32rem;
--max-width-7: 48rem;
--max-width-8: 64rem;
--max-width-9: 96rem;
--border-radius-none: 0;
--border-radius-1: .125rem;
--border-radius-2: .25rem;
--border-radius-3: .5rem;
--border-radius-4: 1rem;
--border-radius-circle: 100%;
--border-radius-pill: 9999px;
--border-width-none: 0;
--border-width-1: .125rem;
--border-width-2: .25rem;
--border-width-3: .5rem;
--border-width-4: 1rem;
--border-width-5: 2rem;
--box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 );
--box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 );
--black: #000;
--near-black: #111;
--dark-gray:#333;
--mid-gray:#555;
--gray: #777;
--silver: #999;
--light-silver: #aaa;
--moon-gray: #ccc;
--light-gray: #eee;
--near-white: #f4f4f4;
--white: #fff;
--transparent: transparent;
--black-90: rgba(0,0,0,.9);
--black-80: rgba(0,0,0,.8);
--black-70: rgba(0,0,0,.7);
--black-60: rgba(0,0,0,.6);
--black-50: rgba(0,0,0,.5);
--black-40: rgba(0,0,0,.4);
--black-30: rgba(0,0,0,.3);
--black-20: rgba(0,0,0,.2);
--black-10: rgba(0,0,0,.1);
--black-05: rgba(0,0,0,.05);
--black-025: rgba(0,0,0,.025);
--black-0125: rgba(0,0,0,.0125);
--white-90: rgba(255,255,255,.9);
--white-80: rgba(255,255,255,.8);
--white-70: rgba(255,255,255,.7);
--white-60: rgba(255,255,255,.6);
--white-50: rgba(255,255,255,.5);
--white-40: rgba(255,255,255,.4);
--white-30: rgba(255,255,255,.3);
--white-20: rgba(255,255,255,.2);
--white-10: rgba(255,255,255,.1);
--white-05: rgba(255,255,255,.05);
--white-025: rgba(255,255,255,.025);
--white-0125: rgba(255,255,255,.0125);
--dark-red: #e7040f;
--red: #ff4136;
--light-red: #ff725c;
--orange: #ff6300;
--gold: #ffb700;
--yellow: #ffd700;
--light-yellow: #fbf1a9;
--purple: #5e2ca5;
--light-purple: #a463f2;
--dark-pink: #d5008f;
--hot-pink: #ff41b4;
--pink: #ff80cc;
--light-pink: #ffa3d7;
--dark-green: #137752;
--green: #19a974;
--light-green: #9eebcf;
--navy: #001b44;
--dark-blue: #00449e;
--blue: #357edd;
--light-blue: #96ccff;
--lightest-blue: #cdecff;
--washed-blue: #f6fffe;
--washed-green: #e8fdf5;
--washed-yellow: #fffceb;
--washed-red: #ffdfdf;
/* PEACHCLOUD-SPECIFIC VARIABLES */
--primary: var(--light-green);
--secondary: var(--near-white);
--success: var(--green);
--info: var(--blue);
--warning: var(--orange);
--danger: var(--red);
--light: var(--light-gray);
--dark: var(--near-black);
/* we need to add shades for each accent colour
*
* --info-100
* --info-200
* --info-300 -> var(--blue)
* --info-400
* --info-500
*/
}

View File

@ -1,971 +0,0 @@
@import url('/static/css/_variables.css');
/* ------------------------------ *\
* peachcloud.css
*
* Index
* - ALIGNMENT
* - BODY
* - BUTTONS
* - CARDS
* - CAPSULES
* - CIRCLES
* - COLORS
* - GRIDS
* - HTML
* - FLASH MESSAGE
* - FONTS
* - ICONS
* - INPUTS
* - LABELS
* - LINKS
* - LISTS
* - MAIN
* - METERS
* - NAVIGATION
* - PARAGRAPHS
* - SWITCHES / SLIDERS
*
\* ------------------------------ */
/*
* ALIGNMENT
*/
.center {
display: block;
margin-left: auto;
margin-right: auto;
}
.center-text {
text-align: center;
}
.center-vert {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.push-right {
margin-left: auto;
padding-right: 0;
}
.top-left {
/* place-self combines align-self and justify-self */
place-self: end center;
}
@media only screen and (min-width: 600px) {
.top-left {
place-self: end;
}
}
.top-right {
place-self: end center;
}
@media only screen and (min-width: 600px) {
.top-right {
place-self: end start;
}
}
.top-middle {
place-self: center;
}
@media only screen and (min-width: 600px) {
.top-middle {
padding-bottom: 2rem;
place-self: center;
}
}
.middle {
place-self: center;
grid-column-start: 1;
grid-column-end: 4;
}
.bottom-middle {
place-self: center;
}
@media only screen and (min-width: 600px) {
.bottom-middle {
padding-top: 2rem;
place-self: center;
}
}
.bottom-left {
place-self: start center;
}
@media only screen and (min-width: 600px) {
.bottom-left {
place-self: start end;
}
}
.bottom-right {
place-self: start center;
}
@media only screen and (min-width: 600px) {
.bottom-right {
place-self: start;
}
}
/*
* BODY
*/
body {
background-color: var(--moon-gray);
height: 100%;
display: flex;
flex-direction: column;
margin: 0;
}
/*
* BUTTONS
*/
.button {
border: 1px solid var(--near-black);
border-radius: var(--border-radius-2);
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: var(--near-black);
cursor: pointer;
padding: 10px;
text-align: center;
text-decoration: none;
font-size: var(--font-size-5);
font-family: var(--sans-serif);
width: 80%;
margin-top: 5px;
margin-bottom: 5px;
}
.button.full-width {
width: 100%;
}
.button-div {
grid-column-start: 1;
grid-column-end: 4;
margin-bottom: 1rem;
}
.button-primary {
background-color: var(--light-gray);
}
.button-primary:hover {
background-color: var(--primary);
}
.button-primary:focus {
background-color: var(--primary);
outline: none;
}
.button-secondary {
background-color: var(--light-gray);
}
.button-secondary:hover {
background-color: var(--light-silver);
}
.button-secondary:focus {
background-color: var(--light-silver);
outline: none;
}
.button-warning {
background-color: var(--light-gray);
}
.button-warning:hover {
background-color: var(--light-red);
}
.button-warning:focus {
background-color: var(--light-red);
outline: none;
}
/*
* CAPSULES
*/
.capsule {
padding: 1rem;
border: var(--border-width-1) solid;
border-radius: var(--border-radius-3);
background-color: var(--light-gray);
/* margin-top: 1rem; */
/* margin-bottom: 1rem; */
}
.capsule-container {
margin-left: 1rem;
margin-right: 1rem;
padding-bottom: 1rem;
}
@media only screen and (min-width: 600px) {
.capsule-container {
margin-left: 0;
margin-right: 0;
}
}
/*
* CARDS
*/
.card {
min-height: 50vh;
max-height: 90vh;
position: relative;
width: 100%;
margin-top: 1rem;
}
@media only screen and (min-width: 600px) {
.card {
min-height: 50vh;
max-height: 90vh;
width: 20rem;
}
}
.card-container {
justify-content: center;
padding: 0.5rem;
}
.form-container {
justify-content: center;
padding-top: 1rem;
padding-bottom: 1rem;
width: 80%;
margin: auto;
}
.text-container {
width: 80%;
margin: auto;
}
.card-text {
margin: 0;
font-size: var(--font-size-5);
padding-bottom: 0.3rem;
}
.container {
display: grid;
grid-template-columns: 2fr 5fr 2fr;
grid-template-rows: auto;
grid-row-gap: 1rem;
align-items: center;
justify-items: center;
margin-bottom: 1rem;
margin-top: 1rem;
}
/*
* CIRCLES
*/
.circle {
align-items: center;
background: var(--light-gray);
border-radius: 50%;
box-shadow: var(--box-shadow-3);
display: flex;
justify-content: center;
position: relative;
}
.circle-small {
height: 5rem;
width: 5rem;
}
.circle-medium {
height: 8rem;
width: 8rem;
}
.circle-large {
height: 13rem;
width: 13rem;
}
.circle-success {
background-color: var(--success);
color: var(--white);
font-size: var(--font-size-4);
}
.circle-warning {
background-color: var(--warning);
color: var(--white);
font-size: var(--font-size-4);
}
.circle-error {
background-color: var(--danger);
color: var(--white);
font-size: var(--font-size-4);
}
/* quartered-circle: circle for the center of radial-menu */
.quartered-circle {
width: 100px;
height: 100px;
}
.quarter {
width: 50%;
height: 50%;
}
.quarter-link {
left: 50%;
margin: -2em;
top: 50%;
}
.quarter-icon {
position: absolute;
bottom: 1em;
left: 1.5em;
}
/*
* COLORS
*/
.primary-bg {
background-color: var(--primary);
}
.secondary-bg {
background-color: var(--secondary);
}
.success-bg {
background-color: var(--success);
}
.info-bg {
background-color: var(--info);
}
.warning-bg {
background-color: var(--warning);
}
.danger-bg {
background-color: var(--danger);
}
.light-bg {
background-color: var(--light);
}
.primary-border {
border-color: var(--primary);
}
.success-border {
border-color: var(--success);
}
.info-border {
border-color: var(--info);
}
.warning-border {
border-color: var(--warning);
}
.danger-border {
border-color: var(--danger);
}
.dark-gray-border {
border-color: var(--dark-gray);
}
/*
* GRIDS
*/
.grid {
display: grid;
grid-template-columns: 2fr 1fr 2fr;
grid-template-rows: 2fr 1fr 2fr;
height: 80vh;
}
.flex-grid {
display: flex;
align-content: space-between;
align-items: baseline;
justify-content: center;
}
.two-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto;
align-items: center;
justify-content: center;
justify-items: center;
padding-bottom: 1rem;
/* margin-right: 2rem; */
/* margin-left: 2rem; */
/* padding-top: 1.5rem; */
}
.two-grid-top-right {
grid-column: 2;
justify-self: right;
padding: 0;
}
.three-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
grid-gap: 10px;
align-items: center;
justify-content: center;
}
.profile-grid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto;
grid-gap: 10px;
align-items: center;
justify-content: center;
justify-items: center;
margin-right: 2rem;
margin-left: 2rem;
padding-top: 1.5rem;
padding-bottom: 1rem;
}
.stack {
display: grid;
align-items: flex-end;
justify-items: center;
justify-content: center;
}
.three-grid-icon-1 {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-icon-2 {
align-self: center;
grid-column: 2;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-icon-3 {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-label-1 {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
text-align: center;
}
.three-grid-label-2 {
align-self: center;
grid-column: 2;
grid-row: 1;
justify-self: center;
text-align: center;
}
.three-grid-label-3 {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
text-align: center;
}
.grid-column-1 {
grid-column: 1;
}
.grid-column-2 {
grid-column: 2;
justify-self: left;
}
.grid-column-3 {
grid-column: 3;
}
/*
* HTML
*/
html {
height: 100%;
}
/*
* FLASH MESSAGE
*/
.flash-message {
font-family: var(--sans-serif);
font-size: var(--font-size-6);
margin-left: 2rem;
margin-right: 2rem;
margin-top: 1rem;
}
/*
* FONTS
*/
.font-near-black {
color: var(--near-black);
}
.font-gray {
color: var(--mid-gray);
}
.font-light-gray {
color: var(--silver);
}
.font-success {
color: var(--success);
}
.font-warning {
color: var(--warning);
}
.font-failure {
color: var(--danger);
}
/*
* ICONS
*/
.icon {
width: 3rem;
}
.icon-small {
width: 1rem;
}
.icon-medium {
width: 2rem;
}
.icon-large {
width: 5rem;
}
.icon-100 {
width: 100%;
}
/* icon-active: sets color of icon svg to near-black */
.icon-active {
filter: invert(0%) sepia(1%) saturate(4171%) hue-rotate(79deg) brightness(86%) contrast(87%);
}
/* icon-inactive: sets color of icon svg to gray */
.icon-inactive {
filter: invert(72%) sepia(8%) saturate(14%) hue-rotate(316deg) brightness(93%) contrast(92%);
}
/*
* INPUTS
*/
.input {
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin-top: 0.5rem;
margin-bottom: 1rem;
padding-left: 5px;
line-height: 1.5rem;
width: 80%;
}
.form-input {
margin-bottom: 0;
margin-left: 0px;
border: 0px;
padding-left: 5px;
line-height: 1.5rem;
width: 100%;
}
.message-input {
height: 7rem;
overflow: auto;
resize: vertical;
}
.alert-input {
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin-right: 0.25rem;
padding-right: 0.25rem;
text-align: right;
width: 7rem;
}
.input-wrapper {
margin-bottom: 15px;
}
/*
* LABELS
*/
.label-small {
font-family: var(--sans-serif);
font-size: var(--font-size-7);
display: block;
}
.label-medium {
font-size: var(--font-size-3);
display: block;
}
.label-large {
font-size: var(--font-size-2);
display: block;
}
.label-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
width: 10rem;
}
.input-label {
margin-bottom: 0.4rem;
}
/*
* LINKS
*/
.link {
text-decoration: none;
color: var(--font-near-black);
}
/*
* LISTS
*/
.list {
padding-left: 0;
margin-left: 0;
max-width: var(--max-width-6);
border: 1px solid var(--light-silver);
border-radius: var(--border-radius-2);
list-style-type: none;
font-family: var(--sans-serif);
}
.list-container {
width: var(--max-width-5);
}
.list-icon {
align-self: center;
justify-self: right;
grid-column: 2;
grid-row: 1/3;
}
.list-item {
display: grid;
padding: 1rem;
border-bottom-color: var(--light-silver);
border-bottom-style: solid;
border-bottom-width: 1px;
}
.list-text {
justify-self: left;
grid-column: 1;
grid-row: 1;
margin: 0;
font-size: var(--font-size-5);
}
.list-label {
justify-self: left;
grid-column: 1;
grid-row: 2;
}
/*
* MAIN
*/
main {
flex: 1 0 auto;
}
/*
* METERS
*/
meter {
border: 1px solid #ccc;
border-radius: 3px;
display: block;
/* height: 1rem; */
margin: 0 auto;
margin-bottom: 1rem;
width: 100%;
/* remove default styling */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* Firefox */
background: none; /* remove default background */
background-color: var(--near-white);
box-shadow: 0 5px 5px -5px #333 inset;
}
meter::-webkit-meter-bar {
background: none; /* remove default background */
background-color: var(--near-white);
box-shadow: 0 5px 5px -5px #333 inset;
}
meter::-webkit-meter-optimum-value {
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
transition: width .5s;
}
/* Firefox styling */
meter::-moz-meter-bar {
background: var(--mid-gray);
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
}
.meter-gauge {
background-color: var(--near-white);
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 5px 5px -5px #333 inset;
display: block;
}
/* Chrome styling */
.meter-gauge > span {
background: var(--mid-gray);
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
display: block;
height: inherit;
text-indent: -9999px;
}
/*
* NAVIGATION
*/
.nav-bar {
display: flex;
align-items: center;
width: 100%;
height: 2em;
padding-top: 1rem;
padding-bottom: 1rem;
justify-content: space-between;
}
.nav-title {
font-family: var(--sans-serif);
font-size: var(--font-size-4);
font-weight: normal;
margin: 0;
}
.nav-icon {
width: auto;
height: 90%;
cursor: pointer;
}
.nav-icon-left {
float: left;
padding-left: 10px;
}
.nav-icon-right {
float: right;
padding-right: 10px;
}
.nav-item {
display: inline-block;
list-style-type: none;
}
/*
* PARAGRAPHS
*/
p {
font-family: var(--sans-serif);
overflow-wrap: anywhere;
}
/*
* SWITCHES / SLIDERS
*/
/* switch: the box around the slider */
.switch {
display: inline-block;
height: 34px;
position: relative;
width: 60px;
}
/* hide default HTML checkbox */
.switch input {
height: 0;
opacity: 0;
width: 0;
}
.switch-icon-left {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
}
.switch-icon-right {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
}
.slider {
background-color: var(--moon-gray);
bottom: 0;
cursor: pointer;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: .4s;
-webkit-transition: .4s;
}
.slider:before {
background-color: var(--white);
bottom: 4px;
content: "";
height: 26px;
left: 4px;
position: absolute;
transition: .4s;
-webkit-transition: .4s;
width: 26px;
}
input:checked + .slider {
background-color: var(--near-black);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--near-black);
}
input:checked + .slider:before {
-ms-transform: translateX(26px);
transform: translateX(26px);
-webkit-transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/*
* TITLES
*/
.title-medium {
font-size: var(--font-size-4);
font-family: var(--sans-serif);
max-width: var(--max-width-6);
}

View File

@ -1 +0,0 @@
yo :)

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
<g>
<g>
<path d="M503.839,395.379l-195.7-338.962C297.257,37.569,277.766,26.315,256,26.315c-21.765,0-41.257,11.254-52.139,30.102
L8.162,395.378c-10.883,18.85-10.883,41.356,0,60.205c10.883,18.849,30.373,30.102,52.139,30.102h391.398
c21.765,0,41.256-11.254,52.14-30.101C514.722,436.734,514.722,414.228,503.839,395.379z M477.861,440.586
c-5.461,9.458-15.241,15.104-26.162,15.104H60.301c-10.922,0-20.702-5.646-26.162-15.104c-5.46-9.458-5.46-20.75,0-30.208
L229.84,71.416c5.46-9.458,15.24-15.104,26.161-15.104c10.92,0,20.701,5.646,26.161,15.104l195.7,338.962
C483.321,419.836,483.321,431.128,477.861,440.586z"/>
</g>
</g>
<g>
<g>
<rect x="241.001" y="176.01" width="29.996" height="149.982"/>
</g>
</g>
<g>
<g>
<path d="M256,355.99c-11.027,0-19.998,8.971-19.998,19.998s8.971,19.998,19.998,19.998c11.026,0,19.998-8.971,19.998-19.998
S267.027,355.99,256,355.99z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 477.175 477.175" style="enable-background:new 0 0 477.175 477.175;" xml:space="preserve">
<g>
<path d="M145.188,238.575l215.5-215.5c5.3-5.3,5.3-13.8,0-19.1s-13.8-5.3-19.1,0l-225.1,225.1c-5.3,5.3-5.3,13.8,0,19.1l225.1,225
c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4c5.3-5.3,5.3-13.8,0-19.1L145.188,238.575z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 768 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M14.5 18h-10c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
<path fill="#000000" d="M16.5 3c-0.276 0-0.5 0.224-0.5 0.5v15c0 0.276-0.224 0.5-0.5 0.5h-11c-0.827 0-1.5-0.673-1.5-1.5s0.673-1.5 1.5-1.5h9c0.827 0 1.5-0.673 1.5-1.5v-12c0-0.827-0.673-1.5-1.5-1.5h-10c-0.827 0-1.5 0.673-1.5 1.5v15c0 1.378 1.122 2.5 2.5 2.5h11c0.827 0 1.5-0.673 1.5-1.5v-15c0-0.276-0.224-0.5-0.5-0.5zM3.5 2h10c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5h-9c-0.562 0-1.082 0.187-1.5 0.501v-13.001c0-0.276 0.224-0.5 0.5-0.5z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 916 B

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M17.5 20h-16c-0.827 0-1.5-0.673-1.5-1.5v-16c0-0.827 0.673-1.5 1.5-1.5h16c0.827 0 1.5 0.673 1.5 1.5v16c0 0.827-0.673 1.5-1.5 1.5zM1.5 2c-0.276 0-0.5 0.224-0.5 0.5v16c0 0.276 0.224 0.5 0.5 0.5h16c0.276 0 0.5-0.224 0.5-0.5v-16c0-0.276-0.224-0.5-0.5-0.5h-16z"></path>
<path fill="#000000" d="M6.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-9c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v9c0 0.276-0.224 0.5-0.5 0.5zM5 16h1v-8h-1v8z"></path>
<path fill="#000000" d="M10.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-12c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5zM9 16h1v-11h-1v11z"></path>
<path fill="#000000" d="M14.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-5c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v5c0 0.276-0.224 0.5-0.5 0.5zM13 16h1v-4h-1v4z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="612px" height="612px" viewBox="0 0 612 612" style="enable-background:new 0 0 612 612;" xml:space="preserve">
<g>
<g id="cloud-off">
<path d="M494.7,229.5c-17.851-86.7-94.351-153-188.7-153c-38.25,0-73.95,10.2-102,30.6l38.25,38.25
c17.85-12.75,40.8-17.85,63.75-17.85c76.5,0,140.25,63.75,140.25,140.25v12.75h38.25c43.35,0,76.5,33.15,76.5,76.5
c0,28.05-15.3,53.55-40.8,66.3l38.25,38.25C591.6,438.6,612,400.35,612,357C612,290.7,558.45,234.6,494.7,229.5z M76.5,109.65
l71.4,68.85C66.3,183.6,0,249.9,0,331.5c0,84.15,68.85,153,153,153h298.35l51,51l33.15-33.15L109.65,76.5L76.5,109.65z
M196.35,229.5l204,204H153c-56.1,0-102-45.9-102-102c0-56.1,45.9-102,102-102H196.35z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<svg height="638pt" viewBox="-20 -129 638.67144 638" width="638pt" xmlns="http://www.w3.org/2000/svg"><path d="m478.90625 132.8125c-4.785156.003906-9.5625.292969-14.3125.863281-12.894531-41.988281-51.628906-70.683593-95.550781-70.773437-10.933594-.011719-21.789063 1.804687-32.121094 5.363281-25.578125-55.308594-86.195313-85.367187-145.699219-72.25-59.511718 13.121094-101.867187 65.875-101.824218 126.808594.003906 10.53125 1.316406 21.019531 3.890624 31.222656-56.695312 8.65625-97.203124 59.46875-92.988281 116.667969 4.207031 57.203125 51.71875 101.542968 109.070313 101.796875h369.535156c66.191406 0 119.847656-53.660157 119.847656-119.851563s-53.65625-119.847656-119.847656-119.847656zm0 219.722656h-369.535156c-49.238282.214844-89.472656-39.253906-90.207032-88.488281-.730468-49.234375 38.304688-89.878906 87.53125-91.132813 3.195313-.089843 6.152344-1.703124 7.957032-4.339843 1.8125-2.640625 2.246094-5.980469 1.171875-8.992188-19.824219-56.730469 9.664062-118.855469 66.152343-139.367187 56.484376-20.511719 118.964844 8.226562 140.15625 64.460937.96875 2.609375 2.976563 4.691407 5.546876 5.753907 2.574218 1.0625 5.46875 1.003906 7.992187-.160157 10.457031-4.863281 21.84375-7.382812 33.371094-7.394531 38 .070312 70.722656 26.835938 78.3125 64.070312 1.085937 5.414063 6.359375 8.914063 11.765625 7.820313 6.511718-1.304687 13.136718-1.96875 19.785156-1.976563 55.160156 0 99.875 44.71875 99.875 99.871094 0 55.160156-44.714844 99.875-99.875 99.875zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M7.631 19.702c-0.041 0-0.083-0.005-0.125-0.016-0.898-0.231-1.761-0.587-2.564-1.059-0.233-0.137-0.315-0.434-0.186-0.671 0.159-0.292 0.243-0.622 0.243-0.957 0-1.103-0.897-2-2-2-0.334 0-0.665 0.084-0.957 0.243-0.237 0.129-0.534 0.047-0.671-0.186-0.472-0.804-0.828-1.666-1.059-2.564-0.065-0.254 0.077-0.515 0.325-0.598 0.814-0.274 1.362-1.036 1.362-1.895s-0.547-1.621-1.362-1.895c-0.248-0.084-0.39-0.344-0.325-0.598 0.231-0.898 0.587-1.761 1.059-2.564 0.137-0.233 0.434-0.315 0.671-0.186 0.291 0.159 0.622 0.243 0.957 0.243 1.103 0 2-0.897 2-2 0-0.334-0.084-0.665-0.243-0.957-0.129-0.237-0.047-0.534 0.186-0.671 0.804-0.472 1.666-0.828 2.564-1.059 0.254-0.065 0.515 0.077 0.598 0.325 0.274 0.814 1.036 1.362 1.895 1.362s1.621-0.547 1.895-1.362c0.084-0.248 0.345-0.39 0.598-0.325 0.898 0.231 1.761 0.587 2.564 1.059 0.233 0.137 0.315 0.434 0.186 0.671-0.159 0.292-0.243 0.622-0.243 0.957 0 1.103 0.897 2 2 2 0.334 0 0.665-0.084 0.957-0.243 0.237-0.129 0.534-0.047 0.671 0.186 0.472 0.804 0.828 1.666 1.059 2.564 0.065 0.254-0.077 0.515-0.325 0.598-0.814 0.274-1.362 1.036-1.362 1.895s0.547 1.621 1.362 1.895c0.248 0.084 0.39 0.344 0.325 0.598-0.231 0.898-0.587 1.761-1.059 2.564-0.137 0.233-0.434 0.315-0.671 0.186-0.292-0.159-0.622-0.243-0.957-0.243-1.103 0-2 0.897-2 2 0 0.334 0.084 0.665 0.243 0.957 0.129 0.237 0.047 0.534-0.186 0.671-0.804 0.472-1.666 0.828-2.564 1.059-0.254 0.065-0.515-0.077-0.598-0.325-0.274-0.814-1.036-1.362-1.895-1.362s-1.621 0.547-1.895 1.362c-0.070 0.207-0.264 0.341-0.474 0.341zM10 17c1.127 0 2.142 0.628 2.655 1.602 0.52-0.161 1.026-0.369 1.51-0.622-0.108-0.314-0.164-0.646-0.164-0.98 0-1.654 1.346-3 3-3 0.334 0 0.666 0.056 0.98 0.164 0.253-0.484 0.462-0.989 0.622-1.51-0.974-0.512-1.602-1.527-1.602-2.655s0.628-2.142 1.602-2.655c-0.161-0.52-0.369-1.026-0.622-1.51-0.314 0.108-0.646 0.164-0.98 0.164-1.654 0-3-1.346-3-3 0-0.334 0.056-0.666 0.164-0.98-0.484-0.253-0.989-0.462-1.51-0.622-0.512 0.974-1.527 1.602-2.655 1.602s-2.142-0.628-2.655-1.602c-0.52 0.16-1.026 0.369-1.51 0.622 0.108 0.314 0.164 0.646 0.164 0.98 0 1.654-1.346 3-3 3-0.334 0-0.666-0.056-0.98-0.164-0.253 0.484-0.462 0.989-0.622 1.51 0.974 0.512 1.602 1.527 1.602 2.655s-0.628 2.142-1.602 2.655c0.16 0.52 0.369 1.026 0.622 1.51 0.314-0.108 0.646-0.164 0.98-0.164 1.654 0 3 1.346 3 3 0 0.334-0.056 0.666-0.164 0.98 0.484 0.253 0.989 0.462 1.51 0.622 0.512-0.974 1.527-1.602 2.655-1.602z"></path>
<path fill="#000000" d="M10 13c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zM10 8c-1.103 0-2 0.897-2 2s0.897 2 2 2c1.103 0 2-0.897 2-2s-0.897-2-2-2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,5 +0,0 @@
<?xml version='1.0' encoding='iso-8859-1'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 457.68 457.68" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 457.68 457.68">
<path d="m439.48,167.086v-111.249c0-17.81-14.49-32.3-32.3-32.3h-374.88c-17.811,0-32.3,14.49-32.3,32.3v226.63c0,17.81 14.49,32.3 32.3,32.3h106.243l-12.162,13.09h-18.221c-4.142,0-7.5,3.358-7.5,7.5s3.358,7.5 7.5,7.5h104.361v72.334c0,10.449 8.501,18.951 18.951,18.951h80.627c10.449,0 18.951-8.501 18.951-18.951v-15.234h100.94c14.166,0 25.69-11.529 25.69-25.7v-182.59c0-11.563-7.674-21.364-18.2-24.581zm3.2,24.581v2.049h-172.49v-2.049c0-5.9 4.8-10.7 10.7-10.7h151.1c5.895,0.001 10.69,4.801 10.69,10.7zm-130.581,63.364h-41.909v-46.315h172.49v148.491h-111.63v-83.226c0-10.449-8.502-18.95-18.951-18.95zm3.951,28.809h-88.528v-9.858c0-2.178 1.772-3.951 3.951-3.951h80.627c2.178,0 3.951,1.772 3.951,3.951v9.858zm108.429-220.503v102.63h-143.59c-14.171,0-25.7,11.529-25.7,25.7v63.364h-23.718c-10.441,0-18.936,8.488-18.949,18.926h-197.523v-210.62h409.48zm-196.959,235.503h88.528v91.495h-88.528v-91.495zm-195.221-260.303h374.88c6.85,2.13163e-14 12.765,4.012 15.565,9.8h-406.011c2.801-5.788 8.716-9.8 15.566-9.8zm-16.025,250.421h196.247v10.81h-180.222c-7.243-0.001-13.452-4.48-16.025-10.81zm130.582,38.899l12.162-13.09h53.503v13.09h-65.665zm165.242,91.286h-80.627c-2.178,0-3.951-1.772-3.951-3.951v-9.857h88.528v9.857c0.001,2.178-1.772,3.951-3.95,3.951zm119.891-34.185h-100.94v-12.75h111.63v2.05c0,5.899-4.795,10.7-10.69,10.7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M441.156,322.876l-48.666-47.386c-3.319-3.243-8.619-3.234-11.93,0.017l-81.894,80.299V8.533
c0-4.71-3.823-8.533-8.533-8.533h-68.267c-4.71,0-8.533,3.823-8.533,8.533v347.273l-81.894-80.299
c-3.311-3.243-8.602-3.251-11.921-0.017l-48.666,47.386c-1.655,1.604-2.586,3.806-2.586,6.11c0,2.304,0.939,4.506,2.586,6.11
l179.2,174.481c1.655,1.613,3.806,2.423,5.948,2.423c2.15,0,4.292-0.811,5.956-2.423l179.2-174.481
c1.647-1.604,2.577-3.806,2.577-6.11C443.733,326.682,442.803,324.48,441.156,322.876z M255.991,491.563L89.028,328.986
l36.412-35.456l90.445,88.695c2.449,2.406,6.11,3.115,9.276,1.775c3.174-1.331,5.231-4.429,5.231-7.868V17.067h51.2v359.066
c0,3.439,2.065,6.537,5.231,7.868c3.166,1.34,6.818,0.631,9.276-1.775l90.445-88.695l36.42,35.456L255.991,491.563z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m218.667969 240h-202.667969c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h202.667969c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m138.667969 320c-4.097657 0-8.191407-1.558594-11.308594-4.691406-6.25-6.253906-6.25-16.386719 0-22.636719l68.695313-68.691406-68.695313-68.671875c-6.25-6.253906-6.25-16.386719 0-22.636719s16.382813-6.25 22.636719 0l80 80c6.25 6.25 6.25 16.382813 0 22.636719l-80 80c-3.136719 3.132812-7.234375 4.691406-11.328125 4.691406zm0 0"/><path d="m341.332031 512c-23.53125 0-42.664062-19.136719-42.664062-42.667969v-384c0-18.238281 11.605469-34.515625 28.882812-40.511719l128.171875-42.730468c28.671875-8.789063 56.277344 12.480468 56.277344 40.578125v384c0 18.21875-11.605469 34.472656-28.863281 40.488281l-128.214844 42.753906c-4.671875 1.449219-9 2.089844-13.589844 2.089844zm128-480c-1.386719 0-2.558593.171875-3.816406.554688l-127.636719 42.558593c-4.183594 1.453125-7.210937 5.675781-7.210937 10.21875v384c0 7.277344 7.890625 12.183594 14.484375 10.113281l127.636718-42.558593c4.160157-1.453125 7.210938-5.675781 7.210938-10.21875v-384c0-5.867188-4.777344-10.667969-10.667969-10.667969zm0 0"/><path d="m186.667969 106.667969c-8.832031 0-16-7.167969-16-16v-32c0-32.363281 26.300781-58.667969 58.664062-58.667969h240c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16h-240c-14.699219 0-26.664062 11.96875-26.664062 26.667969v32c0 8.832031-7.167969 16-16 16zm0 0"/><path d="m314.667969 448h-85.335938c-32.363281 0-58.664062-26.304688-58.664062-58.667969v-32c0-8.832031 7.167969-16 16-16s16 7.167969 16 16v32c0 14.699219 11.964843 26.667969 26.664062 26.667969h85.335938c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M17.5 6h-16c-0.827 0-1.5 0.673-1.5 1.5v9c0 0.827 0.673 1.5 1.5 1.5h16c0.827 0 1.5-0.673 1.5-1.5v-9c0-0.827-0.673-1.5-1.5-1.5zM17.5 7c0.030 0 0.058 0.003 0.087 0.008l-7.532 5.021c-0.29 0.193-0.819 0.193-1.109 0l-7.532-5.021c0.028-0.005 0.057-0.008 0.087-0.008h16zM17.5 17h-16c-0.276 0-0.5-0.224-0.5-0.5v-8.566l7.391 4.927c0.311 0.207 0.71 0.311 1.109 0.311s0.798-0.104 1.109-0.311l7.391-4.927v8.566c0 0.276-0.224 0.5-0.5 0.5z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 777 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M11.5 8c0.276 0 0.5-0.224 0.5-0.5v-4c0-0.827-0.673-1.5-1.5-1.5h-9c-0.827 0-1.5 0.673-1.5 1.5v12c0 0.746 0.537 1.56 1.222 1.853l5.162 2.212c0.178 0.076 0.359 0.114 0.532 0.114 0.213-0 0.416-0.058 0.589-0.172 0.314-0.207 0.495-0.575 0.495-1.008v-1.5h2.5c0.827 0 1.5-0.673 1.5-1.5v-4c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v4c0 0.276-0.224 0.5-0.5 0.5h-2.5v-9.5c0-0.746-0.537-1.56-1.222-1.853l-3.842-1.647h7.564c0.276 0 0.5 0.224 0.5 0.5v4c0 0.276 0.224 0.5 0.5 0.5zM6.384 5.566c0.322 0.138 0.616 0.584 0.616 0.934v12c0 0.104-0.028 0.162-0.045 0.173s-0.081 0.014-0.177-0.027l-5.162-2.212c-0.322-0.138-0.616-0.583-0.616-0.934v-12c0-0.079 0.018-0.153 0.051-0.22l5.333 2.286z"></path>
<path fill="#000000" d="M18.354 9.146l-3-3c-0.195-0.195-0.512-0.195-0.707 0s-0.195 0.512 0 0.707l2.146 2.146h-6.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6.293l-2.146 2.146c-0.195 0.195-0.195 0.512 0 0.707 0.098 0.098 0.226 0.146 0.354 0.146s0.256-0.049 0.354-0.146l3-3c0.195-0.195 0.195-0.512 0-0.707z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M9.5 19c-0.084 0-0.167-0.021-0.243-0.063-0.116-0.065-2.877-1.611-5.369-4.082-0.196-0.194-0.197-0.511-0.003-0.707s0.511-0.197 0.707-0.003c1.979 1.962 4.186 3.346 4.908 3.776 0.723-0.431 2.932-1.817 4.908-3.776 0.196-0.194 0.513-0.193 0.707 0.003s0.193 0.513-0.003 0.707c-2.493 2.471-5.253 4.017-5.369 4.082-0.076 0.042-0.159 0.063-0.243 0.063z"></path>
<path fill="#000000" d="M1.279 11c-0.188 0-0.368-0.106-0.453-0.287-0.548-1.165-0.826-2.33-0.826-3.463 0-2.895 2.355-5.25 5.25-5.25 0.98 0 2.021 0.367 2.931 1.034 0.532 0.39 0.985 0.86 1.319 1.359 0.334-0.499 0.787-0.969 1.319-1.359 0.91-0.667 1.951-1.034 2.931-1.034 2.895 0 5.25 2.355 5.25 5.25 0 1.133-0.278 2.298-0.826 3.463-0.118 0.25-0.415 0.357-0.665 0.24s-0.357-0.415-0.24-0.665c0.485-1.031 0.731-2.053 0.731-3.037 0-2.343-1.907-4.25-4.25-4.25-1.703 0-3.357 1.401-3.776 2.658-0.068 0.204-0.259 0.342-0.474 0.342s-0.406-0.138-0.474-0.342c-0.419-1.257-2.073-2.658-3.776-2.658-2.343 0-4.25 1.907-4.25 4.25 0 0.984 0.246 2.006 0.731 3.037 0.118 0.25 0.010 0.548-0.24 0.665-0.069 0.032-0.141 0.048-0.212 0.048z"></path>
<path fill="#000000" d="M10.515 15c-0.005 0-0.009-0-0.013-0-0.202-0.004-0.569-0.109-0.753-0.766l-1.217-4.334-0.807 3.279c-0.158 0.643-0.525 0.778-0.73 0.8s-0.592-0.027-0.889-0.62l-0.606-1.211c-0.029-0.058-0.056-0.094-0.076-0.117-0.003 0.004-0.007 0.009-0.011 0.015-0.37 0.543-1.192 0.953-1.913 0.953h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.421 0 0.921-0.272 1.087-0.516 0.223-0.327 0.547-0.501 0.891-0.478 0.374 0.025 0.708 0.279 0.917 0.696l0.445 0.89 0.936-3.803c0.158-0.64 0.482-0.779 0.726-0.783s0.572 0.125 0.751 0.76l1.284 4.576 1.178-3.608c0.205-0.628 0.582-0.736 0.788-0.745s0.59 0.068 0.847 0.677l0.724 1.719c0.136 0.322 0.578 0.616 0.927 0.616h1.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-1.5c-0.747 0-1.559-0.539-1.849-1.228l-0.592-1.406-1.274 3.9c-0.207 0.634-0.566 0.733-0.771 0.733z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show More