├── .gitignore ├── src ├── lib.rs ├── error.rs ├── main.rs ├── twitch.rs ├── belabox │ ├── requests.rs │ └── messages.rs ├── monitor.rs ├── bot.rs ├── belabox.rs ├── config.rs └── command_handler.rs ├── Cargo.toml ├── .github └── workflows │ └── release.yml ├── LICENSE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | config.json 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod belabox; 2 | pub mod bot; 3 | mod command_handler; 4 | pub mod config; 5 | pub mod error; 6 | mod monitor; 7 | pub mod twitch; 8 | 9 | pub use belabox::Belabox; 10 | pub use bot::Bot; 11 | use command_handler::CommandHandler; 12 | pub use config::Settings; 13 | use monitor::Monitor; 14 | pub use twitch::Twitch; 15 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{belabox, config, twitch}; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | /// All Errors in this lib. 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum Error { 8 | #[error("BELABOX error")] 9 | Belabox(#[from] belabox::BelaboxError), 10 | #[error("Config error")] 11 | Config(#[from] config::ConfigError), 12 | #[error("Twitch validate error")] 13 | TwitchValide(#[from] twitch_irc::validate::Error), 14 | #[error("Twitch error")] 15 | Twitch(#[from] twitch::TwitchError), 16 | } 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "belabot" 3 | version = "0.4.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | futures-util = "0.3" 10 | read_input = "0.8" 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | serde_with = "3.12.0" 14 | strsim = "0.11.1" 15 | tokio = { version = "1.20.1", features = ["full"] } 16 | tokio-tungstenite = { version = "0.26", features = [ "native-tls" ] } 17 | twitch-irc = "5.0" 18 | 19 | anyhow = "1.0" 20 | thiserror = "2.0.12" 21 | 22 | tracing = "0.1" 23 | tracing-futures = "0.2" 24 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.* 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: taiki-e/create-gh-release-action@v1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | upload-assets: 18 | strategy: 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: taiki-e/upload-rust-binary-action@v1 28 | with: 29 | bin: belabot 30 | tar: unix 31 | zip: windows 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_PROFILE_RELEASE_LTO: true 35 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::Result; 4 | 5 | use belabot::{Bot, Settings}; 6 | use tracing_subscriber::filter::EnvFilter; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | if env::var("RUST_LOG").is_err() { 11 | env::set_var("RUST_LOG", "belabot=info"); 12 | } 13 | 14 | if cfg!(windows) { 15 | tracing_subscriber::fmt() 16 | .with_env_filter(EnvFilter::from_default_env()) 17 | .with_ansi(false) 18 | .init(); 19 | } else { 20 | tracing_subscriber::fmt::init(); 21 | } 22 | 23 | let config = match Settings::load("config.json") { 24 | Ok(c) => c, 25 | Err(_) => Settings::ask_for_settings().await?, 26 | }; 27 | 28 | let bot = Bot::new(config).await?; 29 | 30 | // There is no way to recover when any of these stop, so stop the program 31 | tokio::select! { 32 | _ = bot.bb_msg_handle => {} 33 | _ = bot.tw_msg_handle => {} 34 | }; 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Brian Spit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/twitch.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use thiserror::Error; 4 | use tokio::sync::broadcast; 5 | use tokio::task::JoinHandle; 6 | use tracing::{error, info}; 7 | use twitch_irc::{ 8 | login::StaticLoginCredentials, 9 | message::{self, ServerMessage}, 10 | transport::tcp::{TCPTransport, TLS}, 11 | ClientConfig, SecureTCPTransport, TwitchIRCClient, 12 | }; 13 | 14 | use crate::{config, error::Error}; 15 | 16 | #[derive(Error, Debug)] 17 | pub enum TwitchError { 18 | #[error("disconnected from twitch")] 19 | Disconnected, 20 | #[error("twitch error")] 21 | TwitchIrc(#[from] twitch_irc::Error, StaticLoginCredentials>), 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct HandleMessage { 26 | pub channel_name: String, 27 | pub sender_name: String, 28 | pub broadcaster: bool, 29 | pub moderator: bool, 30 | pub vip: bool, 31 | pub message: String, 32 | } 33 | 34 | pub struct Twitch { 35 | pub read_handle: JoinHandle<()>, 36 | pub client: TwitchIRCClient, StaticLoginCredentials>, 37 | message_tx: Weak>, 38 | channel: String, 39 | } 40 | 41 | impl Twitch { 42 | pub async fn run(settings: config::Twitch) -> Result { 43 | let config::Twitch { 44 | bot_username, 45 | bot_oauth, 46 | channel, 47 | .. 48 | } = settings; 49 | 50 | let username = bot_username.to_lowercase(); 51 | let channel = channel.to_lowercase(); 52 | let mut oauth = bot_oauth; 53 | 54 | if let Some(strip_oauth) = oauth.strip_prefix("oauth:") { 55 | oauth = strip_oauth.to_string(); 56 | } 57 | 58 | let twitch_credentials = StaticLoginCredentials::new(username, Some(oauth)); 59 | let twitch_config = ClientConfig::new_simple(twitch_credentials); 60 | let (mut incoming_messages, client) = 61 | TwitchIRCClient::::new(twitch_config); 62 | 63 | info!("Connected"); 64 | 65 | let (tx, _) = broadcast::channel(100); 66 | let message_tx = Arc::new(tx); 67 | 68 | let tx_read = message_tx.clone(); 69 | let read_handle = tokio::spawn(async move { 70 | while let Some(message) = incoming_messages.recv().await { 71 | match message { 72 | ServerMessage::Notice(msg) => { 73 | error!("{}", msg.message_text); 74 | if msg.message_text == "Login authentication failed" { 75 | break; 76 | } 77 | } 78 | ServerMessage::Privmsg(msg) => { 79 | let _ = tx_read.send(HandleMessage::from(msg)); 80 | } 81 | _ => (), 82 | } 83 | } 84 | }); 85 | 86 | client.join(channel.to_owned())?; 87 | 88 | Ok(Self { 89 | client, 90 | read_handle, 91 | message_tx: Arc::downgrade(&message_tx), 92 | channel, 93 | }) 94 | } 95 | 96 | pub fn message_stream(&self) -> Result, TwitchError> { 97 | let tx = self.message_tx.upgrade().ok_or(TwitchError::Disconnected)?; 98 | 99 | Ok(tx.subscribe()) 100 | } 101 | 102 | pub async fn send(&self, message: String) -> Result<(), TwitchError> { 103 | self.client 104 | .say(self.channel.to_owned(), message) 105 | .await 106 | .map_err(TwitchError::TwitchIrc) 107 | } 108 | } 109 | 110 | impl From for HandleMessage { 111 | fn from(m: message::PrivmsgMessage) -> Self { 112 | let broadcaster = m.badges.contains(&message::Badge { 113 | name: "broadcaster".to_string(), 114 | version: "1".to_string(), 115 | }); 116 | 117 | let moderator = m.badges.contains(&message::Badge { 118 | name: "moderator".to_string(), 119 | version: "1".to_string(), 120 | }); 121 | 122 | let vip = m.badges.contains(&message::Badge { 123 | name: "vip".to_string(), 124 | version: "1".to_string(), 125 | }); 126 | 127 | Self { 128 | channel_name: m.channel_login, 129 | sender_name: m.sender.login, 130 | broadcaster, 131 | moderator, 132 | vip, 133 | message: m.message_text, 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # belabot 2 | 3 | A chat bot alternative to control [belaUI](https://github.com/BELABOX/belaUI) in combination with [BELABOX Cloud](https://cloud.belabox.net) 4 | 5 | ## How do i run this? 6 | 7 | Just download the latest binary from [releases](https://github.com/715209/belabot/releases) and execute it. 8 | 9 | ## Config 10 | 11 | Example of the config that will be automatically generated upon running the binary and saved as `config.json`. 12 | 13 | ```JSON 14 | { 15 | "belabox": { 16 | "remote_key": "your BELABOX Cloud key", 17 | "custom_interface_name": { 18 | "eth0": "ETH", 19 | "usb0": "USB" 20 | }, 21 | "monitor": { 22 | "modems": true, 23 | "notifications": true, 24 | "ups": true, 25 | "network": false, 26 | "ups_plugged_in": 5.1, 27 | "notification_timeout": 30, 28 | "network_timeout": 30 29 | } 30 | }, 31 | "twitch": { 32 | "bot_username": "715209", 33 | "bot_oauth": "oauth:YOUR_OAUTH", 34 | "channel": "715209", 35 | "admins": ["b3ck"] 36 | }, 37 | "commands": { 38 | "Sensor": { 39 | "command": "!bbsensor", 40 | "permission": "Public" 41 | }, 42 | "Stats": { 43 | "command": "!bbs", 44 | "permission": "Public" 45 | }, 46 | "Poweroff": { 47 | "command": "!bbpo", 48 | "permission": "Broadcaster" 49 | }, 50 | "Restart": { 51 | "command": "!bbrs", 52 | "permission": "Broadcaster" 53 | }, 54 | "Bitrate": { 55 | "command": "!bbb", 56 | "permission": "Broadcaster" 57 | }, 58 | "Start": { 59 | "command": "!bbstart", 60 | "permission": "Broadcaster" 61 | }, 62 | "Stop": { 63 | "command": "!bbstop", 64 | "permission": "Broadcaster" 65 | }, 66 | "Network": { 67 | "command": "!bbt", 68 | "permission": "Broadcaster" 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ### BELABOX 75 | 76 | ```JSON 77 | "belabox": { 78 | "remote_key": "key", 79 | "custom_interface_name": { 80 | "eth0": "Something", 81 | "usb0": "Else" 82 | }, 83 | "monitor": { 84 | "modems": true, 85 | "notifications": true, 86 | "ups": true, 87 | "ups_plugged_in": 5.1, 88 | "notification_timeout": 30 89 | } 90 | } 91 | ``` 92 | 93 | - `remote_key`: Your [BELABOX Cloud](https://cloud.belabox.net) key 94 | - `custom_interface_name`: Change the name of the interface 95 | - `monitor`: Enable monitoring for automatic chat messages 96 | 97 | ### Twitch 98 | 99 | ```JSON 100 | "twitch": { 101 | "bot_username": "715209", 102 | "bot_oauth": "oauth:YOUR_OAUTH", 103 | "channel": "715209", 104 | "admins": ["b3ck", "another"] 105 | }, 106 | ``` 107 | 108 | - `bot_username`: The username of your bot account 109 | - `bot_oauth`: The oauth of your bot ([generate an oauth](https://twitchapps.com/tmi)). 110 | - `channel`: The channel the bot should join 111 | - `admins`: Comma sepperated list of twitch usernames, these will have permissions to run all commands 112 | 113 | ### Commands 114 | 115 | ```JSON 116 | "commands": { 117 | "Sensor": { 118 | "command": "!bbsensor", 119 | "permission": "Public" 120 | }, 121 | ... 122 | } 123 | ``` 124 | 125 | - `command`: The chat command 126 | - `permission`: The permission for this command, valid options are: `Public`, `Vip`, `Moderator`, `Broadcaster`. 127 | 128 | ## Chat Commands 129 | 130 | After running the executable successfully you can use the following commands in your chat: 131 | 132 | | Name | Default command | Description | 133 | | ---------- | --------------- | ----------------------------------------------------- | 134 | | Bitrate | !bbb (bitrate) | Sets the max bitrate | 135 | | Network | !bbt (name) | Toggles an interface to disable or enable | 136 | | Poweroff | !bbpo | Poweroff the jetson nano | 137 | | Restart | !bbrestart | Restarts the jetson nano | 138 | | Sensor | !bbsensor | Shows the current sensor information | 139 | | Stats | !bbs | Shows the current connected modems status and bitrate | 140 | | Modems | !bbm | Shows the current connected modems status and bitrate | 141 | | Start | !bbstart | Starts the stream | 142 | | Stop | !bbstop | Stops the stream | 143 | | Latency | !bbl (latency) | Changes the SRT latency in ms | 144 | | AudioDelay | !bbd (delay) | Changes the audio delay in ms | 145 | | AudioSrc | !bba (source) | Changes the audio source | 146 | | Pipeline | !bbp (pipeline) | Changes the pipeline | 147 | 148 | ## Disclaimer 149 | 150 | This is a third party tool, please do not ask for help on the BELABOX discord server. Instead, join the [NOALBS Community Server](https://discord.gg/efWu5HWM2u) for all your questions. 151 | -------------------------------------------------------------------------------- /src/belabox/requests.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | #[serde(rename_all = "camelCase")] 5 | pub enum Request { 6 | Bitrate(Bitrate), 7 | Command(Command), 8 | Keepalive(Option<()>), 9 | Netif(Netif), 10 | Remote(Remote), 11 | Start(Start), 12 | Stop(u8), 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub enum Command { 18 | Poweroff, 19 | Reboot, 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | #[serde(rename_all = "camelCase")] 24 | pub enum Remote { 25 | #[serde(rename = "auth/key")] 26 | AuthKey { key: String, version: u32 }, 27 | #[serde(rename = "auth/token")] 28 | AuthToken { token: String, version: u32 }, 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | #[serde_with::skip_serializing_none] 33 | pub struct Start { 34 | pub pipeline: String, 35 | pub delay: i32, 36 | pub max_br: u32, 37 | pub srt_latency: u64, 38 | pub srt_streamid: Option, 39 | pub srtla_addr: Option, 40 | pub srtla_port: Option, 41 | pub bitrate_overlay: bool, 42 | pub asrc: Option, 43 | pub acodec: Option, 44 | pub remote_key: String, 45 | pub relay_server: Option, 46 | pub relay_account: Option, 47 | } 48 | 49 | impl From for Start { 50 | fn from(c: super::messages::Config) -> Self { 51 | Self { 52 | pipeline: c.pipeline, 53 | delay: c.delay, 54 | max_br: c.max_br, 55 | srt_latency: c.srt_latency, 56 | bitrate_overlay: c.bitrate_overlay, 57 | asrc: c.asrc, 58 | acodec: c.acodec, 59 | remote_key: c.remote_key, 60 | relay_server: c.relay_server, 61 | relay_account: c.relay_account, 62 | srt_streamid: c.srt_streamid, 63 | srtla_addr: c.srtla_addr, 64 | srtla_port: c.srtla_port, 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, Serialize, Deserialize)] 70 | pub struct Bitrate { 71 | pub max_br: u32, 72 | } 73 | 74 | #[derive(Debug, Serialize, Deserialize)] 75 | pub struct Netif { 76 | pub name: String, 77 | pub ip: String, 78 | pub enabled: bool, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn keepalive() { 87 | let message = Request::Keepalive(None); 88 | 89 | let json = serde_json::to_string(&message).unwrap(); 90 | println!("{}", json); 91 | 92 | let expected = r#"{"keepalive":null}"#; 93 | assert_eq!(expected, json); 94 | } 95 | 96 | #[test] 97 | fn start() { 98 | let message = Request::Start(Start { 99 | pipeline: "7ca3d9dd20726a7c2dad06948e1eadc6f84c461c".to_string(), 100 | delay: 0, 101 | max_br: 500, 102 | srt_latency: 4000, 103 | bitrate_overlay: false, 104 | asrc: "No audio".to_string(), 105 | acodec: "opus".to_string(), 106 | remote_key: "remote_key".to_string(), 107 | relay_server: "1".to_string(), 108 | relay_account: "1".to_string(), 109 | }); 110 | 111 | let json = serde_json::to_string(&message).unwrap(); 112 | println!("{}", json); 113 | 114 | let expected = r#"{"start":{"pipeline":"7ca3d9dd20726a7c2dad06948e1eadc6f84c461c","delay":0,"max_br":500,"srt_latency":4000,"bitrate_overlay":false,"asrc":"No audio","acodec":"opus","remote_key":"remote_key","relay_server":"1","relay_account":"1"}}"#; 115 | assert_eq!(expected, json); 116 | } 117 | 118 | #[test] 119 | fn stop() { 120 | let message = Request::Stop(0); 121 | 122 | let json = serde_json::to_string(&message).unwrap(); 123 | println!("{}", json); 124 | 125 | let expected = r#"{"stop":0}"#; 126 | assert_eq!(expected, json); 127 | } 128 | 129 | #[test] 130 | fn bitrate() { 131 | let message = Request::Bitrate(Bitrate { max_br: 1250 }); 132 | 133 | let json = serde_json::to_string(&message).unwrap(); 134 | println!("{}", json); 135 | 136 | let expected = r#"{"bitrate":{"max_br":1250}}"#; 137 | assert_eq!(expected, json); 138 | } 139 | 140 | #[test] 141 | fn reboot() { 142 | let message = Request::Command(Command::Reboot); 143 | 144 | let json = serde_json::to_string(&message).unwrap(); 145 | println!("{}", json); 146 | 147 | let expected = r#"{"command":"reboot"}"#; 148 | assert_eq!(expected, json); 149 | } 150 | 151 | #[test] 152 | fn auth_key() { 153 | let message = Request::Remote(Remote::AuthKey { 154 | key: "testkey".to_string(), 155 | version: 6, 156 | }); 157 | 158 | let json = serde_json::to_string(&message).unwrap(); 159 | println!("{}", json); 160 | 161 | let expected = r#"{"remote":{"auth/key":{"key":"testkey","version":6}}}"#; 162 | assert_eq!(expected, json); 163 | } 164 | 165 | #[test] 166 | fn netif() { 167 | let message = Request::Netif(Netif { 168 | name: "wlan0".to_string(), 169 | ip: "192.168.1.10".to_string(), 170 | enabled: false, 171 | }); 172 | 173 | let json = serde_json::to_string(&message).unwrap(); 174 | println!("{}", json); 175 | 176 | let expected = r#"{"netif":{"name":"wlan0","ip":"192.168.1.10","enabled":false}}"#; 177 | assert_eq!(expected, json); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc, time::Duration}; 2 | 3 | use tokio::{ 4 | sync::{broadcast, Mutex, RwLock}, 5 | time::Instant, 6 | }; 7 | use tracing::{error, warn}; 8 | 9 | use crate::{ 10 | belabox::{self, messages, Message}, 11 | bot::BelaState, 12 | command_handler, config, Belabox, Twitch, 13 | }; 14 | 15 | pub struct Monitor { 16 | pub belabox: Arc, 17 | pub bela_state: Arc>, 18 | pub twitch: Arc, 19 | pub command_handler: Arc>>, 20 | pub custom_interface_name: HashMap, 21 | } 22 | 23 | impl Monitor { 24 | pub async fn run( 25 | &self, 26 | mut messages: broadcast::Receiver, 27 | monitor: config::Monitor, 28 | ) { 29 | while let Ok(message) = messages.recv().await { 30 | match message { 31 | Message::Netif(netif) => { 32 | if monitor.modems { 33 | self.modems(netif).await; 34 | } 35 | 36 | if monitor.network { 37 | self.network(monitor.network_timeout).await; 38 | } 39 | } 40 | Message::Sensors(sensors) => { 41 | if monitor.ups { 42 | self.ups(sensors, monitor.ups_plugged_in).await; 43 | } 44 | } 45 | Message::Notification(messages::Notifications::Show(notification)) => { 46 | if monitor.notifications { 47 | self.notifications(notification, monitor.notification_timeout) 48 | .await; 49 | } 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | 56 | async fn send(&self, message: String) { 57 | if let Err(e) = self.twitch.send(message).await { 58 | error!(?e, "error sending message to twitch"); 59 | } 60 | } 61 | 62 | pub async fn modems(&self, netif: HashMap) { 63 | let read = self.bela_state.read().await; 64 | let previous = match &read.netif { 65 | Some(p) => p, 66 | None => return, 67 | }; 68 | 69 | let netif_name = |n: &String| -> String { 70 | if let Some(custom) = self.custom_interface_name.get(n) { 71 | return custom.to_owned(); 72 | } 73 | if let Some(custom) = netif 74 | .get(n) 75 | .and_then(|iface| self.custom_interface_name.get(&iface.ip)) 76 | { 77 | return custom.to_owned(); 78 | } 79 | 80 | n.to_owned() 81 | }; 82 | 83 | let added = netif 84 | .keys() 85 | .filter(|&n| !previous.contains_key(n)) 86 | .map(netif_name) 87 | .collect::>(); 88 | 89 | let removed = previous 90 | .keys() 91 | .filter(|&n| !netif.contains_key(n)) 92 | .map(netif_name) 93 | .collect::>(); 94 | 95 | let mut message = Vec::new(); 96 | 97 | if !added.is_empty() { 98 | let a = if added.len() > 1 { "are" } else { "is" }; 99 | 100 | message.push(format!("{} {} now connected", added.join(", "), a)); 101 | } 102 | 103 | if !removed.is_empty() { 104 | let a = if removed.len() > 1 { "have" } else { "has" }; 105 | 106 | message.push(format!("{} {} disconnected", removed.join(", "), a)); 107 | } 108 | 109 | if !message.is_empty() { 110 | self.send(format!("BB: {}", message.join(", "))).await; 111 | } 112 | } 113 | 114 | pub async fn ups(&self, sensors: messages::Sensors, plugged_voltage: f64) { 115 | let voltage = match &sensors.soc_voltage { 116 | Some(v) => v, 117 | None => return, 118 | }; 119 | 120 | let voltage = voltage.split_whitespace().next().unwrap(); 121 | let voltage = voltage.parse::().unwrap(); 122 | let plugged_in = (voltage * 100.0).floor() / 100.0 >= plugged_voltage; 123 | 124 | let charging = { 125 | let mut lock = self.bela_state.write().await; 126 | let notify = &mut lock.notify_ups; 127 | 128 | let current_notify = match notify { 129 | Some(n) => *n, 130 | None => plugged_in, 131 | }; 132 | 133 | let charging = match (plugged_in, current_notify) { 134 | (true, false) => Some(true), 135 | (false, true) => Some(false), 136 | _ => None, 137 | }; 138 | 139 | *notify = Some(plugged_in); 140 | 141 | charging 142 | }; 143 | 144 | if let Some(c) = charging { 145 | let a = if !c { "not" } else { "" }; 146 | let msg = format!("BB: UPS {} charging", a); 147 | 148 | self.send(msg).await; 149 | } 150 | } 151 | 152 | pub async fn notifications( 153 | &self, 154 | notification: messages::NotificationShow, 155 | notification_timeout: u64, 156 | ) { 157 | let mut lock = self.bela_state.write().await; 158 | let timeout = &mut lock.notification_timeout; 159 | 160 | let now = Instant::now(); 161 | for notification in notification.show { 162 | if let Some(time) = timeout.get(¬ification.name) { 163 | if time.elapsed() < Duration::from_secs(notification_timeout) { 164 | continue; 165 | } 166 | } 167 | 168 | warn!(notification.msg, "notication"); 169 | 170 | timeout 171 | .entry(notification.name) 172 | .and_modify(|n| *n = now) 173 | .or_insert(now); 174 | 175 | self.send("BB: ".to_owned() + ¬ification.msg).await; 176 | } 177 | } 178 | 179 | pub async fn network(&self, network_timeout: u64) { 180 | { 181 | let mut lock = self.bela_state.write().await; 182 | if !lock.is_streaming { 183 | return; 184 | } 185 | 186 | let timeout = &mut lock.network_timeout; 187 | if timeout.elapsed() < Duration::from_secs(network_timeout) { 188 | return; 189 | } else { 190 | *timeout = Instant::now(); 191 | } 192 | } 193 | 194 | let lock = self.command_handler.lock().await; 195 | let Some(ch) = &*lock else { return }; 196 | let Ok(msg) = ch.stats().await else { return }; 197 | 198 | self.send(msg).await; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/bot.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use tokio::{ 4 | sync::{broadcast::Receiver, Mutex, RwLock}, 5 | task::JoinHandle, 6 | time::{self, Instant}, 7 | }; 8 | 9 | use crate::{ 10 | belabox::{ 11 | self, 12 | messages::{Remote, StatusKind}, 13 | }, 14 | config::{self, BotCommand}, 15 | error::Error, 16 | twitch::HandleMessage, 17 | Belabox, CommandHandler, Monitor, Settings, Twitch, 18 | }; 19 | 20 | pub struct Bot { 21 | pub bb_msg_handle: JoinHandle<()>, 22 | pub bb_monitor_handle: JoinHandle<()>, 23 | pub tw_msg_handle: JoinHandle<()>, 24 | pub twitch: Arc, 25 | pub belabox: Arc, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct BelaState { 30 | pub online: bool, 31 | pub is_streaming: bool, 32 | pub restart: bool, 33 | pub notify_ups: Option, 34 | pub config: Option, 35 | pub netif: Option>, 36 | pub modems: HashMap, 37 | pub sensors: Option, 38 | pub notification_timeout: HashMap, 39 | pub network_timeout: time::Instant, 40 | pub pipelines: Option>, 41 | pub asrcs: Option>, 42 | } 43 | 44 | impl Default for BelaState { 45 | fn default() -> Self { 46 | Self { 47 | network_timeout: Instant::now(), 48 | online: Default::default(), 49 | is_streaming: Default::default(), 50 | restart: Default::default(), 51 | notify_ups: Default::default(), 52 | config: Default::default(), 53 | netif: Default::default(), 54 | modems: Default::default(), 55 | sensors: Default::default(), 56 | notification_timeout: Default::default(), 57 | pipelines: Default::default(), 58 | asrcs: Default::default(), 59 | } 60 | } 61 | } 62 | 63 | impl Bot { 64 | pub async fn new(config: Settings) -> Result { 65 | let twitch = Arc::new(Twitch::run(config.twitch.clone()).await?); 66 | let belabox = Arc::new(Belabox::connect(config.belabox.remote_key.to_owned()).await?); 67 | 68 | // Create state to store BELABOX information 69 | let bela_state = Arc::new(RwLock::new(BelaState::default())); 70 | 71 | // Access to the command handler 72 | let command_handler = Arc::new(Mutex::new(None)); 73 | 74 | // Read BELABOX messages 75 | let bb_msg_handle = tokio::spawn(handle_belabox_messages( 76 | belabox.message_stream()?, 77 | belabox.clone(), 78 | twitch.clone(), 79 | bela_state.clone(), 80 | )); 81 | 82 | let bb_monitor_handle = tokio::spawn(handle_belabox_monitor( 83 | belabox.message_stream()?, 84 | belabox.clone(), 85 | twitch.clone(), 86 | config.belabox.monitor, 87 | bela_state.clone(), 88 | command_handler.clone(), 89 | config.belabox.custom_interface_name.clone(), 90 | )); 91 | 92 | // Read Twitch messages 93 | let tw_msg_handle = tokio::spawn(handle_twitch_messages( 94 | twitch.message_stream()?, 95 | belabox.clone(), 96 | twitch.clone(), 97 | config.commands, 98 | config.belabox.custom_interface_name, 99 | config.twitch.admins, 100 | bela_state, 101 | command_handler, 102 | )); 103 | 104 | Ok(Self { 105 | bb_msg_handle, 106 | bb_monitor_handle, 107 | tw_msg_handle, 108 | twitch, 109 | belabox, 110 | }) 111 | } 112 | } 113 | 114 | async fn handle_belabox_messages( 115 | mut bb_msg: Receiver, 116 | belabox: Arc, 117 | twitch: Arc, 118 | bela_state: Arc>, 119 | ) { 120 | use belabox::Message; 121 | 122 | while let Ok(message) = bb_msg.recv().await { 123 | match message { 124 | Message::Config(config) => { 125 | let mut lock = bela_state.write().await; 126 | lock.config = Some(config); 127 | } 128 | Message::Remote(Remote::RemoteEncoder(remote)) => { 129 | let mut lock = bela_state.write().await; 130 | lock.online = remote.is_encoder_online 131 | } 132 | Message::Netif(netif) => { 133 | let mut lock = bela_state.write().await; 134 | lock.netif = Some(netif); 135 | } 136 | Message::Sensors(sensors) => { 137 | let mut lock = bela_state.write().await; 138 | lock.sensors = Some(sensors); 139 | } 140 | Message::Bitrate(bitrate) => { 141 | let mut lock = bela_state.write().await; 142 | if let Some(config) = &mut lock.config { 143 | config.max_br = bitrate.max_br; 144 | } 145 | } 146 | Message::Status(status) => { 147 | let mut lock = bela_state.write().await; 148 | match status { 149 | StatusKind::Status(s) => { 150 | lock.is_streaming = s.is_streaming; 151 | lock.asrcs = Some(s.asrcs); 152 | lock.modems = s.modems; 153 | } 154 | StatusKind::Asrcs(a) => { 155 | lock.asrcs = Some(a.asrcs); 156 | } 157 | StatusKind::StreamingStatus(ss) => { 158 | lock.is_streaming = ss.is_streaming; 159 | } 160 | StatusKind::Wifi(_) => {} 161 | StatusKind::AvailableUpdates(_) => {} 162 | StatusKind::Modems(incoming) => { 163 | let current_modems = &mut lock.modems; 164 | 165 | for (key, modem) in incoming.modems.iter() { 166 | current_modems 167 | .entry(key.to_string()) 168 | .and_modify(|existing| { 169 | existing.status = modem.status.clone(); 170 | }) 171 | .or_insert_with(|| modem.clone()); 172 | } 173 | 174 | // Remove modems that are not present in the incoming update. 175 | current_modems.retain(|key, _| incoming.modems.contains_key(key)); 176 | } 177 | StatusKind::Updating(_) => {} 178 | }; 179 | 180 | if lock.restart { 181 | lock.restart = false; 182 | 183 | if let Some(config) = &lock.config { 184 | let request = belabox::requests::Start::from(config.to_owned()); 185 | let _ = belabox.start(request).await; 186 | 187 | let msg = "BB: Reboot successful, starting the stream".to_string(); 188 | let _ = twitch.send(msg).await; 189 | } 190 | } 191 | } 192 | Message::Pipelines(pipelines) => { 193 | let mut lock = bela_state.write().await; 194 | lock.pipelines = Some(pipelines); 195 | } 196 | _ => {} 197 | } 198 | } 199 | } 200 | 201 | async fn handle_belabox_monitor( 202 | bb_msg: Receiver, 203 | belabox: Arc, 204 | twitch: Arc, 205 | monitor: config::Monitor, 206 | bela_state: Arc>, 207 | command_handler: Arc>>, 208 | custom_interface_name: HashMap, 209 | ) { 210 | let handler = Monitor { 211 | belabox, 212 | bela_state, 213 | twitch, 214 | command_handler, 215 | custom_interface_name, 216 | }; 217 | handler.run(bb_msg, monitor).await; 218 | } 219 | 220 | #[allow(clippy::too_many_arguments)] 221 | async fn handle_twitch_messages( 222 | tw_msg: Receiver, 223 | belabox: Arc, 224 | twitch: Arc, 225 | commands: HashMap, 226 | custom_interface_name: HashMap, 227 | admins: Vec, 228 | bela_state: Arc>, 229 | command_handler: Arc>>, 230 | ) { 231 | let handler = CommandHandler { 232 | twitch, 233 | belabox, 234 | bela_state, 235 | commands, 236 | custom_interface_name, 237 | admins, 238 | }; 239 | *command_handler.lock().await = Some(handler.clone()); 240 | handler.run(tw_msg).await; 241 | } 242 | -------------------------------------------------------------------------------- /src/belabox.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use futures_util::{ 4 | stream::{SplitSink, SplitStream}, 5 | SinkExt, StreamExt, 6 | }; 7 | use thiserror::Error; 8 | use tokio::{ 9 | net::TcpStream, 10 | sync::{broadcast, mpsc, oneshot, Mutex}, 11 | task::JoinHandle, 12 | time::{self, Duration}, 13 | }; 14 | use tokio_tungstenite::{ 15 | tungstenite::{self, protocol::CloseFrame, Message as TMessage}, 16 | MaybeTlsStream, WebSocketStream, 17 | }; 18 | use tracing::{debug, error, info, trace, warn}; 19 | 20 | pub mod messages; 21 | pub mod requests; 22 | 23 | pub use messages::Message; 24 | pub use requests::Request; 25 | 26 | pub type Writer = SplitSink>, TMessage>; 27 | pub type Reader = SplitStream>>; 28 | 29 | const BELABOX_WS: &str = "wss://remote.belabox.net/ws/remote"; 30 | 31 | #[derive(Error, Debug)] 32 | pub enum BelaboxError { 33 | #[error("websocket error")] 34 | Connect(#[source] tungstenite::Error), 35 | #[error("websocket send error")] 36 | Send(#[source] tungstenite::Error), 37 | #[error("disconnected from BELABOX Cloud")] 38 | Disconnected, 39 | #[error("auth failed")] 40 | AuthFailed, 41 | #[error("Receiver closed")] 42 | ReceiverClosed(#[from] tokio::sync::oneshot::error::RecvError), 43 | #[error("Already restarting")] 44 | AlreadyRestarting, 45 | } 46 | 47 | pub struct Belabox { 48 | pub run_handle: JoinHandle<()>, 49 | pub message_tx: Weak>, 50 | write: mpsc::UnboundedSender, 51 | } 52 | 53 | #[derive(Debug)] 54 | struct InnerMessage { 55 | pub respond: oneshot::Sender>, 56 | pub message: String, 57 | } 58 | 59 | impl Belabox { 60 | pub async fn connect(key: String) -> Result { 61 | let (inner_tx, inner_rx) = mpsc::unbounded_channel(); 62 | let (message_tx, _) = broadcast::channel(100); 63 | let message_tx = Arc::new(message_tx); 64 | 65 | let auth = requests::Remote::AuthKey { key, version: 6 }; 66 | let run_handle = tokio::spawn(run_loop(auth, message_tx.clone(), inner_rx)); 67 | 68 | Ok(Self { 69 | run_handle, 70 | message_tx: Arc::downgrade(&message_tx), 71 | write: inner_tx, 72 | }) 73 | } 74 | 75 | pub fn message_stream(&self) -> Result, BelaboxError> { 76 | let tx = self 77 | .message_tx 78 | .upgrade() 79 | .ok_or(BelaboxError::Disconnected)?; 80 | 81 | Ok(tx.subscribe()) 82 | } 83 | 84 | pub async fn send(&self, request: Request) -> Result<(), BelaboxError> { 85 | let message = serde_json::to_string(&request).unwrap(); 86 | let (tx, rx) = oneshot::channel(); 87 | let inner = InnerMessage { 88 | respond: tx, 89 | message, 90 | }; 91 | 92 | self.write.send(inner).unwrap(); 93 | 94 | rx.await.map_err(BelaboxError::ReceiverClosed)? 95 | } 96 | 97 | pub async fn start(&self, start: requests::Start) -> Result<(), BelaboxError> { 98 | let request = Request::Start(start); 99 | 100 | self.send(request).await 101 | } 102 | 103 | pub async fn stop(&self) -> Result<(), BelaboxError> { 104 | let request = Request::Stop(0); 105 | 106 | self.send(request).await 107 | } 108 | 109 | pub async fn command(&self, command: requests::Command) -> Result<(), BelaboxError> { 110 | let request = Request::Command(command); 111 | 112 | self.send(request).await 113 | } 114 | 115 | pub async fn restart(&self) -> Result<(), BelaboxError> { 116 | self.command(requests::Command::Reboot).await 117 | } 118 | 119 | pub async fn poweroff(&self) -> Result<(), BelaboxError> { 120 | self.command(requests::Command::Poweroff).await 121 | } 122 | 123 | pub async fn bitrate(&self, max_br: u32) -> Result<(), BelaboxError> { 124 | let request = Request::Bitrate(requests::Bitrate { max_br }); 125 | 126 | self.send(request).await 127 | } 128 | 129 | pub async fn netif(&self, network: requests::Netif) -> Result<(), BelaboxError> { 130 | let request = Request::Netif(network); 131 | 132 | self.send(request).await 133 | } 134 | } 135 | 136 | async fn run_loop( 137 | auth: requests::Remote, 138 | message_tx: Arc>, 139 | inner_rx: mpsc::UnboundedReceiver, 140 | ) { 141 | // Spawn thread to handle inner requests 142 | let request_write = Arc::new(Mutex::new(None)); 143 | tokio::spawn(handle_requests(inner_rx, request_write.clone())); 144 | 145 | loop { 146 | let ws_stream = get_connection().await; 147 | let (mut write, read) = ws_stream.split(); 148 | 149 | // Authenticate 150 | let auth_request = serde_json::to_string(&Request::Remote(auth.clone())).unwrap(); 151 | if let Err(e) = write.send(TMessage::Text(auth_request.into())).await { 152 | error!(?e, "error sending auth message"); 153 | continue; 154 | }; 155 | 156 | { 157 | *request_write.lock().await = Some(write); 158 | } 159 | 160 | // Spawn thread to handle keepalive 161 | let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel(); 162 | tokio::spawn(keepalive(request_write.clone(), cancel_rx)); 163 | 164 | // Handle messages 165 | if let Err(BelaboxError::AuthFailed) = handle_messages(read, message_tx.clone()).await { 166 | break; 167 | }; 168 | 169 | // Disconnected 170 | let _ = cancel_tx.send(()); 171 | 172 | { 173 | *request_write.lock().await = None; 174 | } 175 | } 176 | } 177 | 178 | async fn get_connection() -> WebSocketStream> { 179 | let mut retry_grow = 1; 180 | 181 | loop { 182 | info!("Connecting"); 183 | 184 | if let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(BELABOX_WS).await { 185 | info!("Connected"); 186 | break ws_stream; 187 | } 188 | 189 | let wait = 1 << retry_grow; 190 | warn!("Unable to connect"); 191 | info!("trying to connect again in {} seconds", wait); 192 | tokio::time::sleep(Duration::from_secs(wait)).await; 193 | 194 | if retry_grow < 5 { 195 | retry_grow += 1; 196 | } 197 | } 198 | } 199 | 200 | async fn keepalive(write: Arc>>, mut cancel_rx: oneshot::Receiver<()>) { 201 | loop { 202 | time::sleep(Duration::from_secs(5)).await; 203 | 204 | if cancel_rx.try_recv().is_ok() { 205 | debug!("keepalive cancel received"); 206 | break; 207 | } 208 | 209 | debug!("Sending keepalive"); 210 | 211 | if let Some(w) = write.lock().await.as_mut() { 212 | if (w 213 | .send(TMessage::Text( 214 | serde_json::to_string(&Request::Keepalive(None)) 215 | .unwrap() 216 | .into(), 217 | )) 218 | .await) 219 | .is_err() 220 | { 221 | break; 222 | } 223 | } 224 | } 225 | 226 | debug!("Keepalive stopped") 227 | } 228 | 229 | async fn handle_messages( 230 | mut read: Reader, 231 | message_tx: Arc>, 232 | ) -> Result<(), BelaboxError> { 233 | while let Some(Ok(message)) = read.next().await { 234 | if let TMessage::Close(info) = &message { 235 | if let Some(CloseFrame { reason, .. }) = info { 236 | info!(%reason, "connection closed with reason"); 237 | } 238 | 239 | continue; 240 | } 241 | 242 | if let TMessage::Text(text) = &message { 243 | if let Ok(m) = serde_json::from_str::(text) { 244 | handle_message(m, &message_tx).await?; 245 | continue; 246 | } 247 | 248 | let text: serde_json::Value = match serde_json::from_str(text) { 249 | Ok(o) => o, 250 | Err(e) => { 251 | error!(?e, ?text, "failed to deserialize"); 252 | continue; 253 | } 254 | }; 255 | 256 | let text = match text.as_object() { 257 | Some(o) => o, 258 | None => { 259 | error!(?text, "not an object"); 260 | continue; 261 | } 262 | } 263 | .to_owned(); 264 | 265 | for obj in text { 266 | let v: Vec<_> = vec![obj.to_owned()]; 267 | let x: serde_json::Value = v.into_iter().collect(); 268 | 269 | let m: Message = match serde_json::from_value(x) { 270 | Ok(o) => o, 271 | Err(e) => { 272 | error!(?e, ?obj, "failed to deserialize"); 273 | continue; 274 | } 275 | }; 276 | 277 | handle_message(m, &message_tx).await?; 278 | } 279 | } 280 | } 281 | 282 | warn!("Disconnected from BELABOX Cloud"); 283 | 284 | Ok(()) 285 | } 286 | 287 | async fn handle_message( 288 | m: Message, 289 | message_tx: &Arc>, 290 | ) -> Result<(), BelaboxError> { 291 | if let Message::Remote(messages::Remote::RemoteAuth(remote)) = &m { 292 | if !remote.auth_key { 293 | error!("Failed to authenticate"); 294 | return Err(BelaboxError::AuthFailed); 295 | } 296 | } 297 | 298 | trace!(?m, "Received message"); 299 | let _ = message_tx.send(m); 300 | 301 | Ok(()) 302 | } 303 | 304 | // TODO: Add retry or timeout? 305 | async fn handle_requests( 306 | mut inner_rx: mpsc::UnboundedReceiver, 307 | write: Arc>>, 308 | ) { 309 | while let Some(request) = inner_rx.recv().await { 310 | trace!(?request.message, "sending"); 311 | 312 | let mut lock = write.lock().await; 313 | if let Some(w) = lock.as_mut() { 314 | let res = w 315 | .send(TMessage::Text(request.message.into())) 316 | .await 317 | .map_err(BelaboxError::Send); 318 | 319 | request.respond.send(res).unwrap(); 320 | } else { 321 | request 322 | .respond 323 | .send(Err(BelaboxError::Disconnected)) 324 | .unwrap(); 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use read_input::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | use tracing::error; 7 | 8 | const CONFIG_FILE_NAME: &str = "config.json"; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum ConfigError { 12 | #[error("IO Error")] 13 | Io(#[from] std::io::Error), 14 | #[error("Json error: {0}")] 15 | Json(#[from] serde_json::error::Error), 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 19 | pub struct Settings { 20 | pub belabox: Belabox, 21 | pub twitch: Twitch, 22 | pub commands: HashMap, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 26 | #[serde(default)] 27 | pub struct Belabox { 28 | pub remote_key: String, 29 | pub custom_interface_name: HashMap, 30 | pub monitor: Monitor, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Debug, Clone)] 34 | #[serde(default)] 35 | pub struct Monitor { 36 | pub modems: bool, 37 | pub notifications: bool, 38 | pub ups: bool, 39 | pub network: bool, 40 | pub ups_plugged_in: f64, 41 | pub notification_timeout: u64, 42 | pub network_timeout: u64, 43 | } 44 | 45 | impl Default for Monitor { 46 | fn default() -> Self { 47 | Self { 48 | modems: true, 49 | notifications: true, 50 | ups: false, 51 | network: false, 52 | ups_plugged_in: 5.1, 53 | notification_timeout: 30, 54 | network_timeout: 30, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 60 | pub struct Twitch { 61 | pub bot_username: String, 62 | pub bot_oauth: String, 63 | pub channel: String, 64 | pub admins: Vec, 65 | } 66 | 67 | #[derive(Serialize, Deserialize, Debug, Clone)] 68 | pub struct CommandInformation { 69 | pub command: String, 70 | pub permission: Permission, 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] 74 | pub enum BotCommand { 75 | AudioDelay, 76 | AudioSrc, 77 | Bitrate, 78 | Latency, 79 | Modems, 80 | Network, 81 | Pipeline, 82 | Poweroff, 83 | Restart, 84 | Sensor, 85 | Start, 86 | Stats, 87 | Stop, 88 | } 89 | 90 | #[derive(Serialize, Deserialize, Debug, Clone)] 91 | pub enum Permission { 92 | Broadcaster, 93 | Moderator, 94 | Vip, 95 | Public, 96 | } 97 | 98 | impl Settings { 99 | /// Loads the config 100 | pub fn load

(path: P) -> Result 101 | where 102 | P: AsRef, 103 | { 104 | let file = std::fs::read_to_string(path)?; 105 | let mut config = match serde_json::from_str::(&file) { 106 | Ok(c) => c, 107 | Err(e) => { 108 | error!(%e, "config error"); 109 | return Err(ConfigError::Json(e)); 110 | } 111 | }; 112 | 113 | // Lowercase important settings such as the twitch channel name to 114 | // avoid issues. 115 | lowercase_settings(&mut config); 116 | 117 | // Insert chat commands in the config if they don't exist. 118 | default_chat_commands(&mut config.commands); 119 | 120 | std::fs::write(CONFIG_FILE_NAME, serde_json::to_string_pretty(&config)?)?; 121 | 122 | Ok(config) 123 | } 124 | 125 | pub async fn ask_for_settings() -> Result { 126 | println!("Please paste your BELABOX Cloud remote URL below"); 127 | 128 | let remote_key: String = input() 129 | .msg("URL: ") 130 | .add_err_test( 131 | |u: &String| u.contains("?key="), 132 | "No key found, please try again", 133 | ) 134 | .get() 135 | .split("?key=") 136 | .nth(1) 137 | .expect("No key found") 138 | .to_string(); 139 | 140 | let mut custom_interface_name = HashMap::new(); 141 | custom_interface_name.insert("eth0".to_string(), "eth0".to_string()); 142 | custom_interface_name.insert("usb0".to_string(), "usb0".to_string()); 143 | custom_interface_name.insert("wlan0".to_string(), "wlan0".to_string()); 144 | 145 | println!("\nDo you want to receive automatic chat messages about:"); 146 | 147 | let is_y_or_n = |x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n"; 148 | let mut monitor = Monitor { 149 | modems: input_to_bool( 150 | input() 151 | .msg("The status of your modems (Y/n): ") 152 | .add_test(is_y_or_n) 153 | .err("Please enter y or n: ") 154 | .default("y".to_string()) 155 | .get(), 156 | ), 157 | notifications: input_to_bool( 158 | input() 159 | .msg("The belaUI notifications (Y/n): ") 160 | .add_test(is_y_or_n) 161 | .err("Please enter y or n: ") 162 | .default("y".to_string()) 163 | .get(), 164 | ), 165 | ups: input_to_bool( 166 | input() 167 | .msg("The status of your UPS (y/N): ") 168 | .add_test(is_y_or_n) 169 | .err("Please enter y or n: ") 170 | .default("n".to_string()) 171 | .get(), 172 | ), 173 | ..Default::default() 174 | }; 175 | 176 | if monitor.ups { 177 | monitor.ups_plugged_in = input() 178 | .msg("UPS charging threshold (default 5.1 V): ") 179 | .err("Please enter a number") 180 | .default(5.1) 181 | .get(); 182 | } 183 | 184 | let belabox = Belabox { 185 | remote_key, 186 | custom_interface_name, 187 | monitor, 188 | }; 189 | 190 | println!("\nPlease enter your Twitch details below"); 191 | let mut twitch = Twitch { 192 | bot_username: input().msg("Bot username: ").get(), 193 | bot_oauth: input() 194 | .msg("(You can generate an Oauth here: https://twitchapps.com/tmi/)\nBot oauth: ") 195 | .get(), 196 | channel: input().msg("Channel name: ").get(), 197 | admins: Vec::new(), 198 | }; 199 | 200 | let admins = input::() 201 | .msg("Admin users (separate multiple names by a comma): ") 202 | .get(); 203 | 204 | if !admins.is_empty() { 205 | for admin in admins.split(',') { 206 | twitch.admins.push(admin.trim().to_lowercase()); 207 | } 208 | } 209 | 210 | let mut commands = HashMap::new(); 211 | default_chat_commands(&mut commands); 212 | 213 | let mut settings = Self { 214 | belabox, 215 | twitch, 216 | commands, 217 | }; 218 | 219 | std::fs::write(CONFIG_FILE_NAME, serde_json::to_string_pretty(&settings)?)?; 220 | 221 | // FIXME: Does not work on windows 222 | print!("\x1B[2J"); 223 | 224 | let mut path = std::env::current_dir()?; 225 | path.push(CONFIG_FILE_NAME); 226 | println!( 227 | "Saved settings to {} in {}", 228 | CONFIG_FILE_NAME, 229 | path.display() 230 | ); 231 | 232 | lowercase_settings(&mut settings); 233 | 234 | Ok(settings) 235 | } 236 | } 237 | 238 | /// Lowercase settings which should always be lowercase 239 | fn lowercase_settings(settings: &mut Settings) { 240 | let Twitch { 241 | bot_username, 242 | bot_oauth, 243 | channel, 244 | admins, 245 | .. 246 | } = &mut settings.twitch; 247 | 248 | *channel = channel.to_lowercase(); 249 | *bot_oauth = bot_oauth.to_lowercase(); 250 | *bot_username = bot_username.to_lowercase(); 251 | 252 | for user in admins { 253 | *user = user.to_lowercase(); 254 | } 255 | 256 | for info in settings.commands.values_mut() { 257 | info.command = info.command.to_lowercase(); 258 | } 259 | } 260 | 261 | /// Converts y or n to bool. 262 | fn input_to_bool(confirm: String) -> bool { 263 | confirm.to_lowercase() == "y" 264 | } 265 | 266 | // Insert default commands if they don't exist 267 | fn default_chat_commands(commands: &mut HashMap) { 268 | commands 269 | .entry(BotCommand::Start) 270 | .or_insert(CommandInformation { 271 | command: "!bbstart".to_string(), 272 | permission: Permission::Broadcaster, 273 | }); 274 | 275 | commands 276 | .entry(BotCommand::Stop) 277 | .or_insert(CommandInformation { 278 | command: "!bbstop".to_string(), 279 | permission: Permission::Broadcaster, 280 | }); 281 | 282 | commands 283 | .entry(BotCommand::Stats) 284 | .or_insert(CommandInformation { 285 | command: "!bbs".to_string(), 286 | permission: Permission::Public, 287 | }); 288 | 289 | commands 290 | .entry(BotCommand::Modems) 291 | .or_insert(CommandInformation { 292 | command: "!bbm".to_string(), 293 | permission: Permission::Broadcaster, 294 | }); 295 | 296 | commands 297 | .entry(BotCommand::Restart) 298 | .or_insert(CommandInformation { 299 | command: "!bbrs".to_string(), 300 | permission: Permission::Broadcaster, 301 | }); 302 | 303 | commands 304 | .entry(BotCommand::Poweroff) 305 | .or_insert(CommandInformation { 306 | command: "!bbpo".to_string(), 307 | permission: Permission::Broadcaster, 308 | }); 309 | 310 | commands 311 | .entry(BotCommand::Bitrate) 312 | .or_insert(CommandInformation { 313 | command: "!bbb".to_string(), 314 | permission: Permission::Broadcaster, 315 | }); 316 | 317 | commands 318 | .entry(BotCommand::Sensor) 319 | .or_insert(CommandInformation { 320 | command: "!bbsensor".to_string(), 321 | permission: Permission::Public, 322 | }); 323 | 324 | commands 325 | .entry(BotCommand::Network) 326 | .or_insert(CommandInformation { 327 | command: "!bbt".to_string(), 328 | permission: Permission::Broadcaster, 329 | }); 330 | 331 | commands 332 | .entry(BotCommand::Latency) 333 | .or_insert(CommandInformation { 334 | command: "!bbl".to_string(), 335 | permission: Permission::Broadcaster, 336 | }); 337 | 338 | commands 339 | .entry(BotCommand::AudioDelay) 340 | .or_insert(CommandInformation { 341 | command: "!bbd".to_string(), 342 | permission: Permission::Broadcaster, 343 | }); 344 | 345 | commands 346 | .entry(BotCommand::Pipeline) 347 | .or_insert(CommandInformation { 348 | command: "!bbp".to_string(), 349 | permission: Permission::Broadcaster, 350 | }); 351 | 352 | commands 353 | .entry(BotCommand::AudioSrc) 354 | .or_insert(CommandInformation { 355 | command: "!bba".to_string(), 356 | permission: Permission::Broadcaster, 357 | }); 358 | } 359 | -------------------------------------------------------------------------------- /src/belabox/messages.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 6 | #[serde(rename_all = "camelCase")] 7 | pub enum Message { 8 | Config(Config), 9 | Remote(Remote), 10 | Netif(HashMap), 11 | Revisions(Revisions), 12 | Sensors(Sensors), 13 | Status(StatusKind), 14 | Updating(Updating), 15 | Wifi(WifiChange), 16 | Notification(Notifications), 17 | Bitrate(Bitrate), 18 | Pipelines(HashMap), 19 | Acodecs(HashMap), 20 | Relays(Relays), 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 24 | #[serde(rename_all = "camelCase", untagged)] 25 | pub enum Remote { 26 | RemoteAuth(RemoteAuth), 27 | RemoteEncoder(RemoteEncoder), 28 | RemoteRevision(RemoteRevision), 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 32 | pub struct RemoteAuth { 33 | #[serde(rename = "auth/key")] 34 | pub auth_key: bool, 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 38 | pub struct RemoteEncoder { 39 | pub is_encoder_online: bool, 40 | pub version: Option, 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 44 | pub struct RemoteRevision { 45 | pub revision: String, 46 | } 47 | 48 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 49 | pub struct Config { 50 | pub remote_key: String, 51 | pub max_br: u32, 52 | pub delay: i32, 53 | pub pipeline: String, 54 | pub srt_latency: u64, 55 | pub bitrate_overlay: bool, 56 | pub ssh_pass: Option, 57 | pub asrc: Option, 58 | pub acodec: Option, 59 | pub relay_server: Option, 60 | pub relay_account: Option, 61 | pub srt_streamid: Option, 62 | pub srtla_addr: Option, 63 | pub srtla_port: Option, 64 | } 65 | 66 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 67 | pub struct Netif { 68 | pub ip: String, 69 | /// Might have been removed in newer versions 70 | pub txb: Option, 71 | pub tp: u64, 72 | pub enabled: bool, 73 | pub error: Option, 74 | } 75 | 76 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 77 | pub struct Pipeline { 78 | pub acodec: bool, 79 | pub asrc: bool, 80 | pub name: String, 81 | } 82 | 83 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 84 | pub struct StreamingStatus { 85 | pub is_streaming: bool, 86 | } 87 | 88 | #[derive(Debug, Serialize, Clone, PartialEq, Eq)] 89 | pub enum StatusKind { 90 | #[serde(rename = "status")] 91 | Status(Status), 92 | #[serde(rename = "asrcs")] 93 | Asrcs(Asrcs), 94 | #[serde(rename = "is_streaming")] 95 | StreamingStatus(StreamingStatus), 96 | #[serde(rename = "wifi")] 97 | Wifi(WifiChange), 98 | #[serde(rename = "available_updates")] 99 | AvailableUpdates(AvailableUpdatesStatus), 100 | #[serde(rename = "modems")] 101 | Modems(Modems), 102 | #[serde(rename = "updating")] 103 | Updating(Updating), 104 | } 105 | 106 | impl<'de> serde::Deserialize<'de> for StatusKind { 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: serde::Deserializer<'de>, 110 | { 111 | // Deserialize into a generic JSON Value first. 112 | let val = serde_json::Value::deserialize(deserializer)?; 113 | let obj = val.as_object().ok_or_else(|| { 114 | serde::de::Error::custom("Expected a JSON object when deserializing StatusKind") 115 | })?; 116 | 117 | // If more than one key is present, choose the default detailed Status immediately. 118 | if obj.len() > 1 { 119 | return Status::deserialize(val) 120 | .map(StatusKind::Status) 121 | .map_err(serde::de::Error::custom); 122 | } 123 | 124 | // If there's exactly one key, use that key to decide the variant. 125 | if let Some((key, _)) = obj.iter().next() { 126 | return match key.as_str() { 127 | "asrcs" => Asrcs::deserialize(val) 128 | .map(StatusKind::Asrcs) 129 | .map_err(serde::de::Error::custom), 130 | "is_streaming" => StreamingStatus::deserialize(val) 131 | .map(StatusKind::StreamingStatus) 132 | .map_err(serde::de::Error::custom), 133 | "wifi" => WifiChange::deserialize(val) 134 | .map(StatusKind::Wifi) 135 | .map_err(serde::de::Error::custom), 136 | "available_updates" => AvailableUpdatesStatus::deserialize(val) 137 | .map(StatusKind::AvailableUpdates) 138 | .map_err(serde::de::Error::custom), 139 | "modems" => Modems::deserialize(val) 140 | .map(StatusKind::Modems) 141 | .map_err(serde::de::Error::custom), 142 | "updating" => Updating::deserialize(val) 143 | .map(StatusKind::Updating) 144 | .map_err(serde::de::Error::custom), 145 | _ => Status::deserialize(val) 146 | .map(StatusKind::Status) 147 | .map_err(serde::de::Error::custom), 148 | }; 149 | } 150 | 151 | Err(serde::de::Error::custom( 152 | "Expected a single key in the JSON object when deserializing StatusKind", 153 | )) 154 | } 155 | } 156 | 157 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 158 | pub struct Status { 159 | pub is_streaming: bool, 160 | pub available_updates: Option, 161 | pub updating: Option, 162 | pub ssh: Ssh, 163 | pub wifi: HashMap, 164 | pub asrcs: Vec, 165 | pub modems: HashMap, 166 | } 167 | 168 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 169 | pub struct Updating { 170 | pub updating: Update, 171 | } 172 | 173 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 174 | pub struct Update { 175 | pub downloading: u32, 176 | pub unpacking: u32, 177 | pub setting_up: u32, 178 | pub total: u32, 179 | } 180 | 181 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 182 | pub struct WifiChange { 183 | pub wifi: HashMap, 184 | } 185 | 186 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 187 | pub struct AvailableUpdatesStatus { 188 | pub available_updates: Option, 189 | } 190 | 191 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 192 | pub struct AvailableUpdates { 193 | pub package_count: Option, 194 | pub download_size: Option, 195 | } 196 | 197 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 198 | pub struct Ssh { 199 | pub user: String, 200 | pub user_pass: bool, 201 | pub active: bool, 202 | } 203 | 204 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 205 | pub struct Wifi { 206 | pub ifname: String, 207 | pub conn: Option, 208 | pub available: Option>, 209 | pub saved: Option>, 210 | } 211 | 212 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 213 | pub struct Available { 214 | pub active: bool, 215 | pub ssid: String, 216 | pub signal: i64, 217 | pub security: String, 218 | pub freq: i64, 219 | } 220 | 221 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 222 | pub struct Modems { 223 | pub modems: HashMap, 224 | } 225 | 226 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 227 | pub struct Modem { 228 | pub ifname: Option, 229 | pub name: Option, 230 | pub network_type: Option, 231 | pub config: Option, 232 | /// Will be set to true when there is no config 233 | pub no_sim: Option, 234 | // TODO: What does this object look like? 235 | pub available_networks: Option, 236 | pub status: Option, 237 | } 238 | 239 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 240 | pub struct NetworkType { 241 | pub supported: Vec, 242 | pub active: String, 243 | } 244 | 245 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 246 | pub struct ModemConfig { 247 | pub autoconfig: Option, 248 | pub apn: String, 249 | pub username: String, 250 | pub password: String, 251 | pub roaming: bool, 252 | pub network: String, 253 | } 254 | 255 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 256 | pub struct ModemStatus { 257 | pub connection: String, 258 | pub network: Option, 259 | pub network_type: Option, 260 | pub signal: String, 261 | pub roaming: bool, 262 | } 263 | 264 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 265 | pub struct Sensors { 266 | #[serde(rename = "SoC voltage")] 267 | pub soc_voltage: Option, 268 | #[serde(rename = "SoC current")] 269 | pub soc_current: Option, 270 | #[serde(rename = "SoC temperature")] 271 | pub soc_temperature: String, 272 | } 273 | 274 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 275 | pub struct Revisions { 276 | #[serde(rename = "belaUI")] 277 | pub bela_ui: String, 278 | pub belacoder: String, 279 | pub srtla: String, 280 | #[serde(rename = "BELABOX image")] 281 | pub belabox_image: String, 282 | } 283 | 284 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 285 | #[serde(rename_all = "camelCase")] 286 | #[serde(untagged)] 287 | pub enum Notifications { 288 | Show(NotificationShow), 289 | Remove(NotificationRemove), 290 | } 291 | 292 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 293 | pub struct NotificationShow { 294 | pub show: Vec, 295 | } 296 | 297 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 298 | pub struct NotificationRemove { 299 | pub remove: Vec, 300 | } 301 | 302 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 303 | pub struct NotificationMessage { 304 | pub duration: u32, 305 | pub is_dismissable: bool, 306 | pub is_persistent: bool, 307 | pub msg: String, 308 | pub name: String, 309 | #[serde(rename = "type")] 310 | pub kind: String, 311 | } 312 | 313 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 314 | pub struct Bitrate { 315 | pub max_br: u32, 316 | } 317 | 318 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 319 | pub struct Asrcs { 320 | pub asrcs: Vec, 321 | } 322 | 323 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 324 | pub struct Relays { 325 | pub servers: HashMap, 326 | pub accounts: HashMap, 327 | } 328 | 329 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 330 | pub struct Server { 331 | pub name: String, 332 | } 333 | 334 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 335 | pub struct Account { 336 | pub name: String, 337 | } 338 | 339 | #[cfg(test)] 340 | mod tests { 341 | use super::*; 342 | 343 | #[test] 344 | fn asrcs() { 345 | let message = r#"{"status":{"asrcs":["Cam Link 4k","USB audio","No audio"]}}"#; 346 | 347 | let parsed = deserialize(message); 348 | println!("{:#?}", parsed); 349 | 350 | let expected = Message::Status(StatusKind::Asrcs(Asrcs { 351 | asrcs: vec![ 352 | "Cam Link 4k".to_string(), 353 | "USB audio".to_string(), 354 | "No audio".to_string(), 355 | ], 356 | })); 357 | 358 | assert_eq!(parsed, expected); 359 | } 360 | 361 | #[test] 362 | fn is_streaming() { 363 | let message = r#"{"status":{"is_streaming":true}}"#; 364 | 365 | let parsed = deserialize(message); 366 | println!("{:#?}", parsed); 367 | 368 | let expected = Message::Status(StatusKind::StreamingStatus(StreamingStatus { 369 | is_streaming: true, 370 | })); 371 | 372 | assert_eq!(parsed, expected); 373 | } 374 | 375 | #[test] 376 | fn notification_show_empty() { 377 | let message = r#"{"notification":{"show":[]}}"#; 378 | 379 | let parsed = deserialize(message); 380 | println!("{:#?}", parsed); 381 | 382 | let expected = 383 | Message::Notification(Notifications::Show(NotificationShow { show: Vec::new() })); 384 | 385 | assert_eq!(parsed, expected); 386 | } 387 | 388 | #[test] 389 | fn notification_show() { 390 | let message = r#"{"notification":{"show":[{"name":"asrc_not_found","type":"warning","msg":"Selected audio input 'HDMI' is unavailable. Waiting for it before starting the stream...","is_dismissable":false,"is_persistent":true,"duration":2}]}}"#; 391 | 392 | let parsed = deserialize(message); 393 | println!("{:#?}", parsed); 394 | 395 | let expected = Message::Notification(Notifications::Show(NotificationShow { 396 | show: vec![NotificationMessage { 397 | duration: 2, 398 | is_dismissable: false, 399 | is_persistent: true, 400 | msg: "Selected audio input 'HDMI' is unavailable. Waiting for it before starting the stream...".to_string(), 401 | name:"asrc_not_found".to_string(), 402 | kind: "warning".to_string(), 403 | }], 404 | })); 405 | 406 | assert_eq!(parsed, expected); 407 | } 408 | 409 | #[test] 410 | fn notification_remove() { 411 | let message = r#"{"notification":{"remove":["camlink_usb2"]}}"#; 412 | 413 | let parsed = deserialize(message); 414 | println!("{:#?}", parsed); 415 | 416 | let expected = Message::Notification(Notifications::Remove(NotificationRemove { 417 | remove: vec!["camlink_usb2".to_string()], 418 | })); 419 | 420 | assert_eq!(parsed, expected); 421 | } 422 | 423 | fn deserialize(json: &str) -> Message { 424 | serde_json::from_str(json).unwrap() 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/command_handler.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write as _; 2 | use std::{collections::HashMap, sync::Arc}; 3 | 4 | use tokio::sync::{broadcast, RwLock}; 5 | use tracing::{debug, error, info}; 6 | 7 | use crate::{ 8 | belabox::{self, BelaboxError}, 9 | bot::BelaState, 10 | config::{self, BotCommand, Permission}, 11 | error::{Error, Result}, 12 | twitch, Belabox, Twitch, 13 | }; 14 | 15 | #[derive(Clone)] 16 | pub struct CommandHandler { 17 | pub twitch: Arc, 18 | pub belabox: Arc, 19 | pub bela_state: Arc>, 20 | pub commands: HashMap, 21 | pub custom_interface_name: HashMap, 22 | pub admins: Vec, 23 | } 24 | 25 | impl CommandHandler { 26 | pub async fn run(&self, mut messages: broadcast::Receiver) { 27 | while let Ok(hm) = messages.recv().await { 28 | debug!("Handle message: {:?}", hm); 29 | 30 | let mut split_message = hm.message.split_whitespace(); 31 | 32 | // You can't send a blank message.. hopefully 33 | let command = split_message.next().unwrap().to_lowercase(); 34 | let (command, info) = match self.command(command) { 35 | Some(c) => c, 36 | None => continue, 37 | }; 38 | debug!(?command, "found command"); 39 | 40 | if !self.is_allowed_to_execute(&info.permission, &hm) { 41 | continue; 42 | }; 43 | 44 | info!("{} used command {:?}", hm.sender_name, command); 45 | 46 | if !{ self.bela_state.read().await.online } { 47 | self.send("Offline :(".to_string()).await; 48 | continue; 49 | } 50 | 51 | let response = match command { 52 | BotCommand::AudioDelay => self.audio_delay(split_message.next()).await, 53 | BotCommand::AudioSrc => self.audio_src(split_message).await, 54 | BotCommand::Bitrate => self.bitrate(split_message.next()).await, 55 | BotCommand::Latency => self.latency(split_message.next()).await, 56 | BotCommand::Modems => self.modems().await, 57 | BotCommand::Network => self.network(split_message.next()).await, 58 | BotCommand::Pipeline => self.pipeline(split_message).await, 59 | BotCommand::Poweroff => self.poweroff().await, 60 | BotCommand::Restart => self.restart().await, 61 | BotCommand::Sensor => self.sensor().await, 62 | BotCommand::Start => self.start().await, 63 | BotCommand::Stats => self.stats().await, 64 | BotCommand::Stop => self.stop().await, 65 | }; 66 | 67 | match response { 68 | Ok(message) => self.send(message).await, 69 | Err(e) => self.send(format!("Error {}", e)).await, 70 | } 71 | } 72 | } 73 | 74 | async fn send(&self, message: String) { 75 | if let Err(e) = self.twitch.send(message).await { 76 | error!(?e, "error sending message to twitch"); 77 | } 78 | } 79 | 80 | fn command( 81 | &self, 82 | command: String, 83 | ) -> Option<(&config::BotCommand, &config::CommandInformation)> { 84 | self.commands 85 | .iter() 86 | .find(|(_, info)| command == info.command) 87 | } 88 | 89 | fn is_allowed_to_execute( 90 | &self, 91 | permission: &config::Permission, 92 | handle_message: &twitch::HandleMessage, 93 | ) -> bool { 94 | let twitch::HandleMessage { 95 | sender_name, 96 | broadcaster, 97 | moderator, 98 | vip, 99 | .. 100 | } = handle_message; 101 | 102 | let broadcaster = *broadcaster || self.admins.contains(sender_name); 103 | let moderator = broadcaster || *moderator; 104 | let vip = moderator || *vip; 105 | 106 | match permission { 107 | Permission::Broadcaster => broadcaster, 108 | Permission::Moderator => moderator, 109 | Permission::Vip => vip, 110 | Permission::Public => true, 111 | } 112 | } 113 | 114 | pub async fn start(&self) -> Result { 115 | let (config, is_streaming) = { 116 | let read = self.bela_state.read().await; 117 | (read.config.clone(), read.is_streaming) 118 | }; 119 | 120 | let config = match config { 121 | Some(c) => c, 122 | None => { 123 | return Ok("Error starting BELABOX".to_string()); 124 | } 125 | }; 126 | 127 | if is_streaming { 128 | return Ok("Error already streaming".to_string()); 129 | } 130 | 131 | let request = belabox::requests::Start::from(config); 132 | self.belabox.start(request).await?; 133 | 134 | Ok("Starting BELABOX".to_string()) 135 | } 136 | 137 | pub async fn stop(&self) -> Result { 138 | if !{ self.bela_state.read().await.is_streaming } { 139 | return Ok("Error not streaming".to_string()); 140 | } 141 | 142 | self.belabox.stop().await?; 143 | Ok("Stopping BELABOX".to_string()) 144 | } 145 | 146 | pub async fn stats(&self) -> Result { 147 | let (netifs, ups) = { 148 | let read = self.bela_state.read().await; 149 | (read.netif.to_owned(), read.notify_ups) 150 | }; 151 | 152 | let mut total_bitrate = 0; 153 | let mut interfaces = netifs 154 | .iter() 155 | .flatten() 156 | .map(|(mut name, i)| { 157 | let value = if i.enabled { 158 | let bitrate = (i.tp * 8) / 1024; 159 | total_bitrate += bitrate; 160 | format!("{} kbps", bitrate) 161 | } else { 162 | "disabled".to_string() 163 | }; 164 | 165 | // Check if custom interface name based on interface 166 | if let Some(custom) = self.custom_interface_name.get(name) { 167 | name = custom; 168 | } 169 | 170 | // Check if custom interface name based on IP 171 | if let Some(custom) = self.custom_interface_name.get(&i.ip) { 172 | name = custom; 173 | } 174 | 175 | format!("{}: {}", name, value) 176 | }) 177 | .collect::>(); 178 | 179 | // Sort interfaces because they like to move around 180 | interfaces.sort(); 181 | 182 | let mut msg = interfaces.join(", "); 183 | 184 | if interfaces.len() > 1 { 185 | msg = format!("{msg}, Total: {total_bitrate} kbps"); 186 | } 187 | 188 | if let Some(connected) = ups { 189 | let a = if !connected { "not" } else { "" }; 190 | let _ = write!(msg, ", UPS: {} charging", a); 191 | } 192 | 193 | Ok(msg) 194 | } 195 | 196 | pub async fn modems(&self) -> Result { 197 | let (netifs, modems) = { 198 | let state = self.bela_state.read().await; 199 | (state.netif.clone(), state.modems.clone()) 200 | }; 201 | 202 | let modem_infos: Vec = modems 203 | .values() 204 | .map(|modem| { 205 | // Determine the interface name using a custom name if available 206 | // FIXME: The name could be set once in bot.rs 207 | let name_label = if let Some(ifname) = &modem.ifname { 208 | netifs 209 | .as_ref() 210 | .and_then(|netifs| netifs.get(ifname)) 211 | .and_then(|iface| { 212 | self.custom_interface_name 213 | .get(ifname) 214 | .or_else(|| self.custom_interface_name.get(&iface.ip)) 215 | }) 216 | .cloned() 217 | .unwrap_or_else(|| ifname.to_owned()) 218 | } else { 219 | "Modem".to_string() 220 | }; 221 | 222 | // Build status string if available 223 | let status_info = modem.status.as_ref().map_or(String::new(), |status| { 224 | let network_type = status.network_type.as_deref().unwrap_or("?G"); 225 | let network = status.network.as_deref().unwrap_or("Unknown Network"); 226 | 227 | let mut info = format!( 228 | "{} on {}, {}, signal {}", 229 | network_type, network, status.connection, status.signal 230 | ); 231 | if status.roaming { 232 | info.push_str(", roaming"); 233 | } 234 | info 235 | }); 236 | 237 | format!("{}: {}", name_label, status_info).trim().to_owned() 238 | }) 239 | .collect(); 240 | 241 | Ok(modem_infos.join(" - ")) 242 | } 243 | 244 | pub async fn restart(&self) -> Result { 245 | let is_streaming = { 246 | let mut lock = self.bela_state.write().await; 247 | 248 | if lock.restart { 249 | return Err(Error::Belabox(BelaboxError::AlreadyRestarting)); 250 | } 251 | 252 | if lock.is_streaming { 253 | lock.restart = true; 254 | } 255 | 256 | lock.is_streaming 257 | }; 258 | 259 | if is_streaming { 260 | self.belabox.stop().await?; 261 | } 262 | 263 | self.belabox.restart().await?; 264 | Ok("Rebooting BELABOX".to_string()) 265 | } 266 | 267 | pub async fn poweroff(&self) -> Result { 268 | self.belabox.poweroff().await?; 269 | Ok("Powering off BELABOX".to_string()) 270 | } 271 | 272 | pub async fn bitrate(&self, bitrate: Option<&str>) -> Result { 273 | let bitrate = match bitrate { 274 | Some(b) => b, 275 | None => { 276 | return Ok("No bitrate given".to_string()); 277 | } 278 | }; 279 | 280 | let bitrate = match bitrate.parse::() { 281 | Ok(b) => b, 282 | Err(_) => { 283 | return Ok(format!("Invalid number {} given", bitrate)); 284 | } 285 | }; 286 | 287 | if !(500..=12000).contains(&bitrate) { 288 | let msg = format!( 289 | "Invalid value: {}, use a value between 500 - 12000", 290 | bitrate 291 | ); 292 | return Ok(msg); 293 | } 294 | 295 | let bitrate = increment_by_step(bitrate as f64, 250.0) as u32; 296 | self.belabox.bitrate(bitrate).await?; 297 | 298 | { 299 | let mut read = self.bela_state.write().await; 300 | if let Some(config) = &mut read.config { 301 | config.max_br = bitrate; 302 | } 303 | } 304 | 305 | Ok(format!("Changed max bitrate to {} kbps", bitrate)) 306 | } 307 | 308 | pub async fn network(&self, name: Option<&str>) -> Result { 309 | let name = match name { 310 | Some(b) => b.to_lowercase(), 311 | None => { 312 | return Ok("No interface given".to_string()); 313 | } 314 | }; 315 | 316 | let netifs = { 317 | let read = self.bela_state.read().await; 318 | read.netif.to_owned() 319 | }; 320 | 321 | let netifs = match netifs { 322 | Some(n) => n, 323 | None => { 324 | return Ok("Interfaces not available".to_string()); 325 | } 326 | }; 327 | 328 | if netifs.len() == 1 { 329 | return Ok("You only have one connection!".to_string()); 330 | } 331 | 332 | let disabled_count = { 333 | let mut total = 0; 334 | 335 | for v in netifs.values() { 336 | if !v.enabled { 337 | total += 1; 338 | } 339 | } 340 | 341 | total 342 | }; 343 | 344 | let mut interface = netifs.get_key_value(&name); 345 | 346 | if interface.is_none() { 347 | // get iterface name based on custom name 348 | let mut possible_ip = None; 349 | 350 | // Custom name based on interface 351 | for (original, custom) in &self.custom_interface_name { 352 | if name == custom.to_lowercase() { 353 | interface = netifs.get_key_value(original); 354 | possible_ip = Some(original); 355 | break; 356 | } 357 | } 358 | 359 | // Custom name based on ip 360 | if interface.is_none() && possible_ip.is_some() { 361 | let possible_ip = possible_ip.unwrap(); 362 | 363 | for (k, v) in &netifs { 364 | if &v.ip == possible_ip { 365 | interface = netifs.get_key_value(k); 366 | break; 367 | } 368 | } 369 | } 370 | } 371 | 372 | let (interface_name, interface) = match interface { 373 | Some(i) => i, 374 | None => { 375 | return Ok("Interface not found".to_string()); 376 | } 377 | }; 378 | 379 | if netifs.len() - disabled_count == 1 && interface.enabled { 380 | return Ok("Can't disable all networks".to_string()); 381 | } 382 | 383 | let enabled = !interface.enabled; 384 | let network = belabox::requests::Netif { 385 | name: interface_name.to_owned(), 386 | ip: interface.ip.to_owned(), 387 | enabled, 388 | }; 389 | self.belabox.netif(network).await?; 390 | 391 | Ok(format!( 392 | "{} has been {}", 393 | name, 394 | if enabled { "enabled" } else { "disabled" } 395 | )) 396 | } 397 | 398 | pub async fn sensor(&self) -> Result { 399 | let sensors = { 400 | let read = self.bela_state.read().await; 401 | read.sensors.to_owned() 402 | }; 403 | 404 | let sensors = match sensors { 405 | Some(s) => s, 406 | None => { 407 | return Ok("Sensors not available".to_string()); 408 | } 409 | }; 410 | 411 | let belabox::messages::Sensors { 412 | soc_voltage, 413 | soc_current, 414 | soc_temperature, 415 | } = sensors; 416 | 417 | let mut response = format!("Temp: {}", soc_temperature); 418 | 419 | if let Some(voltage) = soc_voltage { 420 | let _ = write!(response, ", Voltage: {}", voltage); 421 | } 422 | 423 | if let Some(current) = soc_current { 424 | let _ = write!(response, ", Amps: {}", current); 425 | } 426 | 427 | Ok(response) 428 | } 429 | 430 | pub async fn latency(&self, latency: Option<&str>) -> Result { 431 | let latency = match latency { 432 | Some(b) => b, 433 | None => { 434 | let current_latency = { 435 | self.bela_state 436 | .read() 437 | .await 438 | .config 439 | .as_ref() 440 | .map(|config| config.srt_latency) 441 | }; 442 | 443 | let latency = if let Some(current) = current_latency { 444 | current.to_string() 445 | } else { 446 | "unknown".to_string() 447 | }; 448 | 449 | return Ok(format!("Current SRT latency is {} ms", latency)); 450 | } 451 | }; 452 | 453 | let latency = match latency.parse::() { 454 | Ok(l) => l, 455 | Err(_) => { 456 | return Ok(format!("Invalid number {} given", latency)); 457 | } 458 | }; 459 | 460 | if !(100..=4000).contains(&latency) { 461 | let msg = format!("Invalid value: {}, use a value between 100 - 4000", latency); 462 | return Ok(msg); 463 | } 464 | 465 | let latency = increment_by_step(latency as f64, 100.0); 466 | let is_streaming = { self.bela_state.read().await.is_streaming }; 467 | 468 | if is_streaming { 469 | let _ = self.stop().await?; 470 | self.send("Restarting the stream".to_string()).await; 471 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await 472 | } 473 | 474 | { 475 | let mut lock = self.bela_state.write().await; 476 | 477 | if let Some(config) = &mut lock.config { 478 | config.srt_latency = latency as u64; 479 | } 480 | } 481 | 482 | if is_streaming { 483 | let _ = self.start().await?; 484 | } 485 | 486 | Ok(format!("Changed SRT latency to {} ms", latency)) 487 | } 488 | 489 | pub async fn audio_delay(&self, delay: Option<&str>) -> Result { 490 | let delay = match delay { 491 | Some(b) => b, 492 | None => { 493 | let current_delay = { 494 | self.bela_state 495 | .read() 496 | .await 497 | .config 498 | .as_ref() 499 | .map(|config| config.delay) 500 | }; 501 | 502 | let delay = if let Some(current) = current_delay { 503 | current.to_string() 504 | } else { 505 | "unknown".to_string() 506 | }; 507 | 508 | return Ok(format!("Current audio delay is {} ms", delay)); 509 | } 510 | }; 511 | 512 | let delay = match delay.parse::() { 513 | Ok(l) => l, 514 | Err(_) => { 515 | return Ok(format!("Invalid number {} given", delay)); 516 | } 517 | }; 518 | 519 | if delay.abs() > 2000 { 520 | let msg = format!("Invalid value: {}, use a value between -2000 - 2000", delay); 521 | return Ok(msg); 522 | } 523 | 524 | let delay = increment_by_step(delay, 20.0); 525 | let is_streaming = { self.bela_state.read().await.is_streaming }; 526 | 527 | if is_streaming { 528 | let _ = self.stop().await?; 529 | self.send("Restarting the stream".to_string()).await; 530 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await 531 | } 532 | 533 | { 534 | let mut lock = self.bela_state.write().await; 535 | 536 | if let Some(config) = &mut lock.config { 537 | config.delay = delay as i32; 538 | } 539 | } 540 | 541 | if is_streaming { 542 | let _ = self.start().await?; 543 | } 544 | 545 | Ok(format!("Changed audio delay to {} ms", delay)) 546 | } 547 | 548 | pub(crate) async fn pipeline<'a, I>(&self, args: I) -> Result 549 | where 550 | I: IntoIterator, 551 | { 552 | let args = args.into_iter(); 553 | let query = args.collect::>().join(" "); 554 | 555 | let (is_streaming, pipelines) = { 556 | let state = self.bela_state.read().await; 557 | let current_pipeline = state.config.as_ref().map(|config| &config.pipeline); 558 | let mut pipelines = Vec::new(); 559 | 560 | if let (Some(all_pipelines), Some(current)) = (&state.pipelines, current_pipeline) { 561 | // Should always contain a "/" and the current pipeline 562 | let current = all_pipelines 563 | .get(current) 564 | .unwrap() 565 | .name 566 | .split('/') 567 | .next() 568 | .unwrap(); 569 | 570 | pipelines = all_pipelines 571 | .iter() 572 | .filter(|(_, v)| v.name.contains(current)) 573 | .map(|(k, v)| (k.to_string(), v.name.split('/').nth(1).unwrap().to_owned())) 574 | .collect(); 575 | }; 576 | 577 | (state.is_streaming, pipelines) 578 | }; 579 | 580 | if is_streaming { 581 | let _ = self.stop().await?; 582 | self.send("Restarting the stream".to_string()).await; 583 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await 584 | } 585 | 586 | // find pipeline 587 | let found_pipeline = pipelines 588 | .iter() 589 | .map(|(h, p)| { 590 | let pl = p.to_lowercase().replace('_', " "); 591 | ((h, p), strsim::sorensen_dice(&query, &pl)) 592 | }) 593 | // .collect::>(); 594 | .min_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); 595 | 596 | let found_pipeline = match found_pipeline { 597 | Some(p) => p, 598 | None => return Ok("Pipeline not found".to_string()), 599 | }; 600 | 601 | if found_pipeline.1 == 0.0 { 602 | return Ok("Pipeline not found".to_string()); 603 | } 604 | 605 | // change pipeline 606 | { 607 | let mut state = self.bela_state.write().await; 608 | if let Some(config) = state.config.as_mut() { 609 | config.pipeline = found_pipeline.0 .0.to_owned(); 610 | } 611 | } 612 | 613 | if is_streaming { 614 | let _ = self.start().await?; 615 | } 616 | 617 | Ok(format!("Changed pipeline to {}", found_pipeline.0 .1)) 618 | } 619 | 620 | pub(crate) async fn audio_src<'a, I>(&self, args: I) -> Result 621 | where 622 | I: IntoIterator, 623 | { 624 | let args = args.into_iter(); 625 | let query = args.collect::>().join(" "); 626 | 627 | let (is_streaming, asrcs) = { 628 | let state = self.bela_state.read().await; 629 | let asrcs = state.asrcs.to_owned(); 630 | 631 | (state.is_streaming, asrcs) 632 | }; 633 | 634 | if is_streaming { 635 | let _ = self.stop().await?; 636 | self.send("Restarting the stream".to_string()).await; 637 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await 638 | } 639 | 640 | let asrcs = match asrcs { 641 | Some(a) => a, 642 | None => return Ok("No audio sources found".to_string()), 643 | }; 644 | 645 | // find audio src 646 | let found_asrcs = asrcs 647 | .iter() 648 | .map(|asrc| (asrc, strsim::sorensen_dice(&query, &asrc.to_lowercase()))) 649 | .min_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); 650 | 651 | let found_asrcs = match found_asrcs { 652 | Some(p) => p, 653 | None => return Ok("Audio source not found".to_string()), 654 | }; 655 | 656 | if found_asrcs.1 == 0.0 { 657 | return Ok("Audio source not found".to_string()); 658 | } 659 | 660 | // change audio src 661 | { 662 | let mut state = self.bela_state.write().await; 663 | if let Some(config) = state.config.as_mut() { 664 | config.asrc = Some(found_asrcs.0.to_owned()); 665 | } 666 | } 667 | 668 | if is_streaming { 669 | let _ = self.start().await?; 670 | } 671 | 672 | Ok(format!("Changed audio to {}", found_asrcs.0)) 673 | } 674 | } 675 | 676 | fn increment_by_step(value: V, step: S) -> f64 677 | where 678 | V: Into, 679 | S: Into, 680 | { 681 | let value = value.into(); 682 | let step = step.into(); 683 | 684 | (value / step).round() * step 685 | } 686 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anyhow" 46 | version = "1.0.97" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 49 | 50 | [[package]] 51 | name = "async-trait" 52 | version = "0.1.88" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 55 | dependencies = [ 56 | "proc-macro2", 57 | "quote", 58 | "syn", 59 | ] 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.4.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 66 | 67 | [[package]] 68 | name = "backtrace" 69 | version = "0.3.74" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 72 | dependencies = [ 73 | "addr2line", 74 | "cfg-if", 75 | "libc", 76 | "miniz_oxide", 77 | "object", 78 | "rustc-demangle", 79 | "windows-targets", 80 | ] 81 | 82 | [[package]] 83 | name = "base64" 84 | version = "0.22.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 87 | 88 | [[package]] 89 | name = "belabot" 90 | version = "0.4.0" 91 | dependencies = [ 92 | "anyhow", 93 | "futures-util", 94 | "read_input", 95 | "serde", 96 | "serde_json", 97 | "serde_with", 98 | "strsim", 99 | "thiserror 2.0.12", 100 | "tokio", 101 | "tokio-tungstenite", 102 | "tracing", 103 | "tracing-futures", 104 | "tracing-subscriber", 105 | "twitch-irc", 106 | ] 107 | 108 | [[package]] 109 | name = "bitflags" 110 | version = "2.9.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 113 | 114 | [[package]] 115 | name = "block-buffer" 116 | version = "0.10.4" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 119 | dependencies = [ 120 | "generic-array", 121 | ] 122 | 123 | [[package]] 124 | name = "bumpalo" 125 | version = "3.17.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 128 | 129 | [[package]] 130 | name = "bytes" 131 | version = "1.10.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 134 | 135 | [[package]] 136 | name = "cc" 137 | version = "1.2.18" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" 140 | dependencies = [ 141 | "shlex", 142 | ] 143 | 144 | [[package]] 145 | name = "cfg-if" 146 | version = "1.0.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 149 | 150 | [[package]] 151 | name = "chrono" 152 | version = "0.4.40" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 155 | dependencies = [ 156 | "android-tzdata", 157 | "iana-time-zone", 158 | "num-traits", 159 | "serde", 160 | "windows-link", 161 | ] 162 | 163 | [[package]] 164 | name = "core-foundation" 165 | version = "0.9.4" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 168 | dependencies = [ 169 | "core-foundation-sys", 170 | "libc", 171 | ] 172 | 173 | [[package]] 174 | name = "core-foundation-sys" 175 | version = "0.8.7" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 178 | 179 | [[package]] 180 | name = "cpufeatures" 181 | version = "0.2.17" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 184 | dependencies = [ 185 | "libc", 186 | ] 187 | 188 | [[package]] 189 | name = "crypto-common" 190 | version = "0.1.6" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 193 | dependencies = [ 194 | "generic-array", 195 | "typenum", 196 | ] 197 | 198 | [[package]] 199 | name = "darling" 200 | version = "0.20.11" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 203 | dependencies = [ 204 | "darling_core", 205 | "darling_macro", 206 | ] 207 | 208 | [[package]] 209 | name = "darling_core" 210 | version = "0.20.11" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 213 | dependencies = [ 214 | "fnv", 215 | "ident_case", 216 | "proc-macro2", 217 | "quote", 218 | "strsim", 219 | "syn", 220 | ] 221 | 222 | [[package]] 223 | name = "darling_macro" 224 | version = "0.20.11" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 227 | dependencies = [ 228 | "darling_core", 229 | "quote", 230 | "syn", 231 | ] 232 | 233 | [[package]] 234 | name = "data-encoding" 235 | version = "2.8.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" 238 | 239 | [[package]] 240 | name = "deranged" 241 | version = "0.4.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 244 | dependencies = [ 245 | "powerfmt", 246 | "serde", 247 | ] 248 | 249 | [[package]] 250 | name = "digest" 251 | version = "0.10.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 254 | dependencies = [ 255 | "block-buffer", 256 | "crypto-common", 257 | ] 258 | 259 | [[package]] 260 | name = "either" 261 | version = "1.15.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 264 | 265 | [[package]] 266 | name = "enum_dispatch" 267 | version = "0.3.13" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 270 | dependencies = [ 271 | "once_cell", 272 | "proc-macro2", 273 | "quote", 274 | "syn", 275 | ] 276 | 277 | [[package]] 278 | name = "equivalent" 279 | version = "1.0.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 282 | 283 | [[package]] 284 | name = "errno" 285 | version = "0.3.11" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 288 | dependencies = [ 289 | "libc", 290 | "windows-sys 0.59.0", 291 | ] 292 | 293 | [[package]] 294 | name = "fastrand" 295 | version = "2.3.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 298 | 299 | [[package]] 300 | name = "fnv" 301 | version = "1.0.7" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 304 | 305 | [[package]] 306 | name = "foreign-types" 307 | version = "0.3.2" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 310 | dependencies = [ 311 | "foreign-types-shared", 312 | ] 313 | 314 | [[package]] 315 | name = "foreign-types-shared" 316 | version = "0.1.1" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 319 | 320 | [[package]] 321 | name = "futures-core" 322 | version = "0.3.31" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 325 | 326 | [[package]] 327 | name = "futures-macro" 328 | version = "0.3.31" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 331 | dependencies = [ 332 | "proc-macro2", 333 | "quote", 334 | "syn", 335 | ] 336 | 337 | [[package]] 338 | name = "futures-sink" 339 | version = "0.3.31" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 342 | 343 | [[package]] 344 | name = "futures-task" 345 | version = "0.3.31" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 348 | 349 | [[package]] 350 | name = "futures-util" 351 | version = "0.3.31" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 354 | dependencies = [ 355 | "futures-core", 356 | "futures-macro", 357 | "futures-sink", 358 | "futures-task", 359 | "pin-project-lite", 360 | "pin-utils", 361 | "slab", 362 | ] 363 | 364 | [[package]] 365 | name = "generic-array" 366 | version = "0.14.7" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 369 | dependencies = [ 370 | "typenum", 371 | "version_check", 372 | ] 373 | 374 | [[package]] 375 | name = "getrandom" 376 | version = "0.3.2" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 379 | dependencies = [ 380 | "cfg-if", 381 | "libc", 382 | "r-efi", 383 | "wasi 0.14.2+wasi-0.2.4", 384 | ] 385 | 386 | [[package]] 387 | name = "gimli" 388 | version = "0.31.1" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 391 | 392 | [[package]] 393 | name = "hashbrown" 394 | version = "0.12.3" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 397 | 398 | [[package]] 399 | name = "hashbrown" 400 | version = "0.15.2" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 403 | 404 | [[package]] 405 | name = "hex" 406 | version = "0.4.3" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 409 | 410 | [[package]] 411 | name = "http" 412 | version = "1.3.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 415 | dependencies = [ 416 | "bytes", 417 | "fnv", 418 | "itoa", 419 | ] 420 | 421 | [[package]] 422 | name = "httparse" 423 | version = "1.10.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 426 | 427 | [[package]] 428 | name = "iana-time-zone" 429 | version = "0.1.63" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 432 | dependencies = [ 433 | "android_system_properties", 434 | "core-foundation-sys", 435 | "iana-time-zone-haiku", 436 | "js-sys", 437 | "log", 438 | "wasm-bindgen", 439 | "windows-core", 440 | ] 441 | 442 | [[package]] 443 | name = "iana-time-zone-haiku" 444 | version = "0.1.2" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 447 | dependencies = [ 448 | "cc", 449 | ] 450 | 451 | [[package]] 452 | name = "ident_case" 453 | version = "1.0.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 456 | 457 | [[package]] 458 | name = "indexmap" 459 | version = "1.9.3" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 462 | dependencies = [ 463 | "autocfg", 464 | "hashbrown 0.12.3", 465 | "serde", 466 | ] 467 | 468 | [[package]] 469 | name = "indexmap" 470 | version = "2.9.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 473 | dependencies = [ 474 | "equivalent", 475 | "hashbrown 0.15.2", 476 | "serde", 477 | ] 478 | 479 | [[package]] 480 | name = "itoa" 481 | version = "1.0.15" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 484 | 485 | [[package]] 486 | name = "js-sys" 487 | version = "0.3.77" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 490 | dependencies = [ 491 | "once_cell", 492 | "wasm-bindgen", 493 | ] 494 | 495 | [[package]] 496 | name = "lazy_static" 497 | version = "1.5.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 500 | 501 | [[package]] 502 | name = "libc" 503 | version = "0.2.171" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 506 | 507 | [[package]] 508 | name = "linux-raw-sys" 509 | version = "0.9.3" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 512 | 513 | [[package]] 514 | name = "lock_api" 515 | version = "0.4.12" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 518 | dependencies = [ 519 | "autocfg", 520 | "scopeguard", 521 | ] 522 | 523 | [[package]] 524 | name = "log" 525 | version = "0.4.27" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 528 | 529 | [[package]] 530 | name = "matchers" 531 | version = "0.1.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 534 | dependencies = [ 535 | "regex-automata 0.1.10", 536 | ] 537 | 538 | [[package]] 539 | name = "memchr" 540 | version = "2.7.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 543 | 544 | [[package]] 545 | name = "miniz_oxide" 546 | version = "0.8.7" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" 549 | dependencies = [ 550 | "adler2", 551 | ] 552 | 553 | [[package]] 554 | name = "mio" 555 | version = "1.0.3" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 558 | dependencies = [ 559 | "libc", 560 | "wasi 0.11.0+wasi-snapshot-preview1", 561 | "windows-sys 0.52.0", 562 | ] 563 | 564 | [[package]] 565 | name = "native-tls" 566 | version = "0.2.14" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 569 | dependencies = [ 570 | "libc", 571 | "log", 572 | "openssl", 573 | "openssl-probe", 574 | "openssl-sys", 575 | "schannel", 576 | "security-framework", 577 | "security-framework-sys", 578 | "tempfile", 579 | ] 580 | 581 | [[package]] 582 | name = "nu-ansi-term" 583 | version = "0.46.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 586 | dependencies = [ 587 | "overload", 588 | "winapi", 589 | ] 590 | 591 | [[package]] 592 | name = "num-conv" 593 | version = "0.1.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 596 | 597 | [[package]] 598 | name = "num-traits" 599 | version = "0.2.19" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 602 | dependencies = [ 603 | "autocfg", 604 | ] 605 | 606 | [[package]] 607 | name = "object" 608 | version = "0.36.7" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 611 | dependencies = [ 612 | "memchr", 613 | ] 614 | 615 | [[package]] 616 | name = "once_cell" 617 | version = "1.21.3" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 620 | 621 | [[package]] 622 | name = "openssl" 623 | version = "0.10.72" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 626 | dependencies = [ 627 | "bitflags", 628 | "cfg-if", 629 | "foreign-types", 630 | "libc", 631 | "once_cell", 632 | "openssl-macros", 633 | "openssl-sys", 634 | ] 635 | 636 | [[package]] 637 | name = "openssl-macros" 638 | version = "0.1.1" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 641 | dependencies = [ 642 | "proc-macro2", 643 | "quote", 644 | "syn", 645 | ] 646 | 647 | [[package]] 648 | name = "openssl-probe" 649 | version = "0.1.6" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 652 | 653 | [[package]] 654 | name = "openssl-sys" 655 | version = "0.9.107" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 658 | dependencies = [ 659 | "cc", 660 | "libc", 661 | "pkg-config", 662 | "vcpkg", 663 | ] 664 | 665 | [[package]] 666 | name = "overload" 667 | version = "0.1.1" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 670 | 671 | [[package]] 672 | name = "parking_lot" 673 | version = "0.12.3" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 676 | dependencies = [ 677 | "lock_api", 678 | "parking_lot_core", 679 | ] 680 | 681 | [[package]] 682 | name = "parking_lot_core" 683 | version = "0.9.10" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 686 | dependencies = [ 687 | "cfg-if", 688 | "libc", 689 | "redox_syscall", 690 | "smallvec", 691 | "windows-targets", 692 | ] 693 | 694 | [[package]] 695 | name = "pin-project" 696 | version = "1.1.10" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 699 | dependencies = [ 700 | "pin-project-internal", 701 | ] 702 | 703 | [[package]] 704 | name = "pin-project-internal" 705 | version = "1.1.10" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "syn", 712 | ] 713 | 714 | [[package]] 715 | name = "pin-project-lite" 716 | version = "0.2.16" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 719 | 720 | [[package]] 721 | name = "pin-utils" 722 | version = "0.1.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 725 | 726 | [[package]] 727 | name = "pkg-config" 728 | version = "0.3.32" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 731 | 732 | [[package]] 733 | name = "powerfmt" 734 | version = "0.2.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 737 | 738 | [[package]] 739 | name = "ppv-lite86" 740 | version = "0.2.21" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 743 | dependencies = [ 744 | "zerocopy", 745 | ] 746 | 747 | [[package]] 748 | name = "proc-macro2" 749 | version = "1.0.94" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 752 | dependencies = [ 753 | "unicode-ident", 754 | ] 755 | 756 | [[package]] 757 | name = "quote" 758 | version = "1.0.40" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 761 | dependencies = [ 762 | "proc-macro2", 763 | ] 764 | 765 | [[package]] 766 | name = "r-efi" 767 | version = "5.2.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 770 | 771 | [[package]] 772 | name = "rand" 773 | version = "0.9.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 776 | dependencies = [ 777 | "rand_chacha", 778 | "rand_core", 779 | "zerocopy", 780 | ] 781 | 782 | [[package]] 783 | name = "rand_chacha" 784 | version = "0.9.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 787 | dependencies = [ 788 | "ppv-lite86", 789 | "rand_core", 790 | ] 791 | 792 | [[package]] 793 | name = "rand_core" 794 | version = "0.9.3" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 797 | dependencies = [ 798 | "getrandom", 799 | ] 800 | 801 | [[package]] 802 | name = "read_input" 803 | version = "0.8.6" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "2f178674da3d005db760b30d6735a989d692da37b86337daec6f2e311223d608" 806 | 807 | [[package]] 808 | name = "redox_syscall" 809 | version = "0.5.11" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 812 | dependencies = [ 813 | "bitflags", 814 | ] 815 | 816 | [[package]] 817 | name = "regex" 818 | version = "1.11.1" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 821 | dependencies = [ 822 | "aho-corasick", 823 | "memchr", 824 | "regex-automata 0.4.9", 825 | "regex-syntax 0.8.5", 826 | ] 827 | 828 | [[package]] 829 | name = "regex-automata" 830 | version = "0.1.10" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 833 | dependencies = [ 834 | "regex-syntax 0.6.29", 835 | ] 836 | 837 | [[package]] 838 | name = "regex-automata" 839 | version = "0.4.9" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 842 | dependencies = [ 843 | "aho-corasick", 844 | "memchr", 845 | "regex-syntax 0.8.5", 846 | ] 847 | 848 | [[package]] 849 | name = "regex-syntax" 850 | version = "0.6.29" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 853 | 854 | [[package]] 855 | name = "regex-syntax" 856 | version = "0.8.5" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 859 | 860 | [[package]] 861 | name = "rustc-demangle" 862 | version = "0.1.24" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 865 | 866 | [[package]] 867 | name = "rustix" 868 | version = "1.0.5" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 871 | dependencies = [ 872 | "bitflags", 873 | "errno", 874 | "libc", 875 | "linux-raw-sys", 876 | "windows-sys 0.59.0", 877 | ] 878 | 879 | [[package]] 880 | name = "rustversion" 881 | version = "1.0.20" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 884 | 885 | [[package]] 886 | name = "ryu" 887 | version = "1.0.20" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 890 | 891 | [[package]] 892 | name = "schannel" 893 | version = "0.1.27" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 896 | dependencies = [ 897 | "windows-sys 0.59.0", 898 | ] 899 | 900 | [[package]] 901 | name = "scopeguard" 902 | version = "1.2.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 905 | 906 | [[package]] 907 | name = "security-framework" 908 | version = "2.11.1" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 911 | dependencies = [ 912 | "bitflags", 913 | "core-foundation", 914 | "core-foundation-sys", 915 | "libc", 916 | "security-framework-sys", 917 | ] 918 | 919 | [[package]] 920 | name = "security-framework-sys" 921 | version = "2.14.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 924 | dependencies = [ 925 | "core-foundation-sys", 926 | "libc", 927 | ] 928 | 929 | [[package]] 930 | name = "serde" 931 | version = "1.0.219" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 934 | dependencies = [ 935 | "serde_derive", 936 | ] 937 | 938 | [[package]] 939 | name = "serde_derive" 940 | version = "1.0.219" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 943 | dependencies = [ 944 | "proc-macro2", 945 | "quote", 946 | "syn", 947 | ] 948 | 949 | [[package]] 950 | name = "serde_json" 951 | version = "1.0.140" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 954 | dependencies = [ 955 | "itoa", 956 | "memchr", 957 | "ryu", 958 | "serde", 959 | ] 960 | 961 | [[package]] 962 | name = "serde_with" 963 | version = "3.12.0" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" 966 | dependencies = [ 967 | "base64", 968 | "chrono", 969 | "hex", 970 | "indexmap 1.9.3", 971 | "indexmap 2.9.0", 972 | "serde", 973 | "serde_derive", 974 | "serde_json", 975 | "serde_with_macros", 976 | "time", 977 | ] 978 | 979 | [[package]] 980 | name = "serde_with_macros" 981 | version = "3.12.0" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" 984 | dependencies = [ 985 | "darling", 986 | "proc-macro2", 987 | "quote", 988 | "syn", 989 | ] 990 | 991 | [[package]] 992 | name = "sha1" 993 | version = "0.10.6" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 996 | dependencies = [ 997 | "cfg-if", 998 | "cpufeatures", 999 | "digest", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "sharded-slab" 1004 | version = "0.1.7" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1007 | dependencies = [ 1008 | "lazy_static", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "shlex" 1013 | version = "1.3.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1016 | 1017 | [[package]] 1018 | name = "signal-hook-registry" 1019 | version = "1.4.2" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1022 | dependencies = [ 1023 | "libc", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "slab" 1028 | version = "0.4.9" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1031 | dependencies = [ 1032 | "autocfg", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "smallvec" 1037 | version = "1.15.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1040 | 1041 | [[package]] 1042 | name = "socket2" 1043 | version = "0.5.9" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1046 | dependencies = [ 1047 | "libc", 1048 | "windows-sys 0.52.0", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "strsim" 1053 | version = "0.11.1" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1056 | 1057 | [[package]] 1058 | name = "syn" 1059 | version = "2.0.100" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1062 | dependencies = [ 1063 | "proc-macro2", 1064 | "quote", 1065 | "unicode-ident", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "tempfile" 1070 | version = "3.19.1" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1073 | dependencies = [ 1074 | "fastrand", 1075 | "getrandom", 1076 | "once_cell", 1077 | "rustix", 1078 | "windows-sys 0.59.0", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "thiserror" 1083 | version = "1.0.69" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1086 | dependencies = [ 1087 | "thiserror-impl 1.0.69", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "thiserror" 1092 | version = "2.0.12" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1095 | dependencies = [ 1096 | "thiserror-impl 2.0.12", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "thiserror-impl" 1101 | version = "1.0.69" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1104 | dependencies = [ 1105 | "proc-macro2", 1106 | "quote", 1107 | "syn", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "thiserror-impl" 1112 | version = "2.0.12" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1115 | dependencies = [ 1116 | "proc-macro2", 1117 | "quote", 1118 | "syn", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "thread_local" 1123 | version = "1.1.8" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1126 | dependencies = [ 1127 | "cfg-if", 1128 | "once_cell", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "time" 1133 | version = "0.3.41" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1136 | dependencies = [ 1137 | "deranged", 1138 | "itoa", 1139 | "num-conv", 1140 | "powerfmt", 1141 | "serde", 1142 | "time-core", 1143 | "time-macros", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "time-core" 1148 | version = "0.1.4" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1151 | 1152 | [[package]] 1153 | name = "time-macros" 1154 | version = "0.2.22" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1157 | dependencies = [ 1158 | "num-conv", 1159 | "time-core", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "tokio" 1164 | version = "1.44.2" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1167 | dependencies = [ 1168 | "backtrace", 1169 | "bytes", 1170 | "libc", 1171 | "mio", 1172 | "parking_lot", 1173 | "pin-project-lite", 1174 | "signal-hook-registry", 1175 | "socket2", 1176 | "tokio-macros", 1177 | "windows-sys 0.52.0", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "tokio-macros" 1182 | version = "2.5.0" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1185 | dependencies = [ 1186 | "proc-macro2", 1187 | "quote", 1188 | "syn", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "tokio-native-tls" 1193 | version = "0.3.1" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1196 | dependencies = [ 1197 | "native-tls", 1198 | "tokio", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "tokio-stream" 1203 | version = "0.1.17" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 1206 | dependencies = [ 1207 | "futures-core", 1208 | "pin-project-lite", 1209 | "tokio", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "tokio-tungstenite" 1214 | version = "0.26.2" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" 1217 | dependencies = [ 1218 | "futures-util", 1219 | "log", 1220 | "native-tls", 1221 | "tokio", 1222 | "tokio-native-tls", 1223 | "tungstenite", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "tokio-util" 1228 | version = "0.7.14" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1231 | dependencies = [ 1232 | "bytes", 1233 | "futures-core", 1234 | "futures-sink", 1235 | "pin-project-lite", 1236 | "tokio", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "tracing" 1241 | version = "0.1.41" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1244 | dependencies = [ 1245 | "pin-project-lite", 1246 | "tracing-attributes", 1247 | "tracing-core", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "tracing-attributes" 1252 | version = "0.1.28" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "syn", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "tracing-core" 1263 | version = "0.1.33" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1266 | dependencies = [ 1267 | "once_cell", 1268 | "valuable", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "tracing-futures" 1273 | version = "0.2.5" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1276 | dependencies = [ 1277 | "pin-project", 1278 | "tracing", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "tracing-log" 1283 | version = "0.2.0" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1286 | dependencies = [ 1287 | "log", 1288 | "once_cell", 1289 | "tracing-core", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "tracing-subscriber" 1294 | version = "0.3.19" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1297 | dependencies = [ 1298 | "matchers", 1299 | "nu-ansi-term", 1300 | "once_cell", 1301 | "regex", 1302 | "sharded-slab", 1303 | "smallvec", 1304 | "thread_local", 1305 | "tracing", 1306 | "tracing-core", 1307 | "tracing-log", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "tungstenite" 1312 | version = "0.26.2" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" 1315 | dependencies = [ 1316 | "bytes", 1317 | "data-encoding", 1318 | "http", 1319 | "httparse", 1320 | "log", 1321 | "native-tls", 1322 | "rand", 1323 | "sha1", 1324 | "thiserror 2.0.12", 1325 | "utf-8", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "twitch-irc" 1330 | version = "5.0.1" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "ba8e5095c3a4d4d72decb36b2ceb1e40085e7d8d93282b1d2f3cf2bba0dee4db" 1333 | dependencies = [ 1334 | "async-trait", 1335 | "bytes", 1336 | "chrono", 1337 | "either", 1338 | "enum_dispatch", 1339 | "futures-util", 1340 | "smallvec", 1341 | "thiserror 1.0.69", 1342 | "tokio", 1343 | "tokio-native-tls", 1344 | "tokio-stream", 1345 | "tokio-util", 1346 | "tracing", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "typenum" 1351 | version = "1.18.0" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1354 | 1355 | [[package]] 1356 | name = "unicode-ident" 1357 | version = "1.0.18" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1360 | 1361 | [[package]] 1362 | name = "utf-8" 1363 | version = "0.7.6" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1366 | 1367 | [[package]] 1368 | name = "valuable" 1369 | version = "0.1.1" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1372 | 1373 | [[package]] 1374 | name = "vcpkg" 1375 | version = "0.2.15" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1378 | 1379 | [[package]] 1380 | name = "version_check" 1381 | version = "0.9.5" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1384 | 1385 | [[package]] 1386 | name = "wasi" 1387 | version = "0.11.0+wasi-snapshot-preview1" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1390 | 1391 | [[package]] 1392 | name = "wasi" 1393 | version = "0.14.2+wasi-0.2.4" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1396 | dependencies = [ 1397 | "wit-bindgen-rt", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "wasm-bindgen" 1402 | version = "0.2.100" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1405 | dependencies = [ 1406 | "cfg-if", 1407 | "once_cell", 1408 | "rustversion", 1409 | "wasm-bindgen-macro", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "wasm-bindgen-backend" 1414 | version = "0.2.100" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1417 | dependencies = [ 1418 | "bumpalo", 1419 | "log", 1420 | "proc-macro2", 1421 | "quote", 1422 | "syn", 1423 | "wasm-bindgen-shared", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "wasm-bindgen-macro" 1428 | version = "0.2.100" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1431 | dependencies = [ 1432 | "quote", 1433 | "wasm-bindgen-macro-support", 1434 | ] 1435 | 1436 | [[package]] 1437 | name = "wasm-bindgen-macro-support" 1438 | version = "0.2.100" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1441 | dependencies = [ 1442 | "proc-macro2", 1443 | "quote", 1444 | "syn", 1445 | "wasm-bindgen-backend", 1446 | "wasm-bindgen-shared", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "wasm-bindgen-shared" 1451 | version = "0.2.100" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1454 | dependencies = [ 1455 | "unicode-ident", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "winapi" 1460 | version = "0.3.9" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1463 | dependencies = [ 1464 | "winapi-i686-pc-windows-gnu", 1465 | "winapi-x86_64-pc-windows-gnu", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "winapi-i686-pc-windows-gnu" 1470 | version = "0.4.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1473 | 1474 | [[package]] 1475 | name = "winapi-x86_64-pc-windows-gnu" 1476 | version = "0.4.0" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1479 | 1480 | [[package]] 1481 | name = "windows-core" 1482 | version = "0.61.0" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 1485 | dependencies = [ 1486 | "windows-implement", 1487 | "windows-interface", 1488 | "windows-link", 1489 | "windows-result", 1490 | "windows-strings", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "windows-implement" 1495 | version = "0.60.0" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1498 | dependencies = [ 1499 | "proc-macro2", 1500 | "quote", 1501 | "syn", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "windows-interface" 1506 | version = "0.59.1" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1509 | dependencies = [ 1510 | "proc-macro2", 1511 | "quote", 1512 | "syn", 1513 | ] 1514 | 1515 | [[package]] 1516 | name = "windows-link" 1517 | version = "0.1.1" 1518 | source = "registry+https://github.com/rust-lang/crates.io-index" 1519 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1520 | 1521 | [[package]] 1522 | name = "windows-result" 1523 | version = "0.3.2" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1526 | dependencies = [ 1527 | "windows-link", 1528 | ] 1529 | 1530 | [[package]] 1531 | name = "windows-strings" 1532 | version = "0.4.0" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1535 | dependencies = [ 1536 | "windows-link", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "windows-sys" 1541 | version = "0.52.0" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1544 | dependencies = [ 1545 | "windows-targets", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "windows-sys" 1550 | version = "0.59.0" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1553 | dependencies = [ 1554 | "windows-targets", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "windows-targets" 1559 | version = "0.52.6" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1562 | dependencies = [ 1563 | "windows_aarch64_gnullvm", 1564 | "windows_aarch64_msvc", 1565 | "windows_i686_gnu", 1566 | "windows_i686_gnullvm", 1567 | "windows_i686_msvc", 1568 | "windows_x86_64_gnu", 1569 | "windows_x86_64_gnullvm", 1570 | "windows_x86_64_msvc", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "windows_aarch64_gnullvm" 1575 | version = "0.52.6" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1578 | 1579 | [[package]] 1580 | name = "windows_aarch64_msvc" 1581 | version = "0.52.6" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1584 | 1585 | [[package]] 1586 | name = "windows_i686_gnu" 1587 | version = "0.52.6" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1590 | 1591 | [[package]] 1592 | name = "windows_i686_gnullvm" 1593 | version = "0.52.6" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1596 | 1597 | [[package]] 1598 | name = "windows_i686_msvc" 1599 | version = "0.52.6" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1602 | 1603 | [[package]] 1604 | name = "windows_x86_64_gnu" 1605 | version = "0.52.6" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1608 | 1609 | [[package]] 1610 | name = "windows_x86_64_gnullvm" 1611 | version = "0.52.6" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1614 | 1615 | [[package]] 1616 | name = "windows_x86_64_msvc" 1617 | version = "0.52.6" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1620 | 1621 | [[package]] 1622 | name = "wit-bindgen-rt" 1623 | version = "0.39.0" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1626 | dependencies = [ 1627 | "bitflags", 1628 | ] 1629 | 1630 | [[package]] 1631 | name = "zerocopy" 1632 | version = "0.8.24" 1633 | source = "registry+https://github.com/rust-lang/crates.io-index" 1634 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1635 | dependencies = [ 1636 | "zerocopy-derive", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "zerocopy-derive" 1641 | version = "0.8.24" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1644 | dependencies = [ 1645 | "proc-macro2", 1646 | "quote", 1647 | "syn", 1648 | ] 1649 | --------------------------------------------------------------------------------