Compare commits
1 Commits
main
...
59afbdab7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 59afbdab7b |
@@ -16,3 +16,13 @@ pub struct ServerStatus {
|
|||||||
pub max_users: Option<u32>,
|
pub max_users: Option<u32>,
|
||||||
pub bandwidth: Option<u32>,
|
pub bandwidth: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
||||||
|
pub struct ServerEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub address: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub username: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
+112
-2
@@ -1,7 +1,9 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||||
|
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Desktop platform implementation using Tokio and native audio.
|
/// Desktop platform implementation using Tokio and native audio.
|
||||||
@@ -37,6 +39,32 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
super::connect::get_status(client).await
|
super::connect::get_status(client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
mumble_udp_ping(address, port).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_server(server: &str) -> Option<()> {
|
||||||
|
let mut config = load_config_map();
|
||||||
|
config.insert("server".to_string(), server.to_string());
|
||||||
|
save_config_map(&config).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
let config = load_config_map();
|
||||||
|
config
|
||||||
|
.get("servers")
|
||||||
|
.and_then(|s| serde_json::from_str(s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
let mut config = load_config_map();
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
config.insert("servers".to_string(), json);
|
||||||
|
let _ = save_config_map(&config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
@@ -56,3 +84,85 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
// No-op on desktop
|
// No-op on desktop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> std::path::PathBuf {
|
||||||
|
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||||
|
top_level_domain: "xyz".to_string(),
|
||||||
|
author: "ohea".to_string(),
|
||||||
|
app_name: "Mumble Web2".to_string(),
|
||||||
|
})
|
||||||
|
.expect("failed to choose app strategy");
|
||||||
|
strategy.config_dir().join("config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_map() -> HashMap<String, String> {
|
||||||
|
let config_path = get_config_path();
|
||||||
|
match std::fs::read_to_string(&config_path) {
|
||||||
|
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
||||||
|
Err(_) => HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
||||||
|
let config_path = get_config_path();
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let contents = serde_json::to_string_pretty(config)?;
|
||||||
|
std::fs::write(&config_path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mumble UDP ping protocol.
|
||||||
|
///
|
||||||
|
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
||||||
|
/// Receive a 24-byte response: 4 bytes version (1 byte each: major.minor.patch + padding)
|
||||||
|
/// + 8 bytes identifier echo + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
||||||
|
async fn mumble_udp_ping(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
let dest = format!("{}:{}", address, port)
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| color_eyre::eyre::eyre!("could not resolve address"))?;
|
||||||
|
|
||||||
|
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
||||||
|
let socket = UdpSocket::bind(bind_addr).await?;
|
||||||
|
socket.connect(dest).await?;
|
||||||
|
|
||||||
|
// Build ping packet: 4 zero bytes + 8-byte request ID
|
||||||
|
let request_id: u64 = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos() as u64;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 12];
|
||||||
|
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
||||||
|
socket.send(&buf).await?;
|
||||||
|
|
||||||
|
let mut response = [0u8; 24];
|
||||||
|
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
||||||
|
|
||||||
|
match timeout {
|
||||||
|
Ok(Ok(len)) if len >= 24 => {
|
||||||
|
let version_major = response[0] as u32;
|
||||||
|
let version_minor = response[1] as u32;
|
||||||
|
let version_patch = response[2] as u32;
|
||||||
|
let users = u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
||||||
|
let max_users = u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
||||||
|
let bandwidth = u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
||||||
|
|
||||||
|
Ok(ServerStatus {
|
||||||
|
success: true,
|
||||||
|
version: Some((version_major, version_minor, version_patch)),
|
||||||
|
users: Some(users),
|
||||||
|
max_users: Some(max_users),
|
||||||
|
bandwidth: Some(bandwidth),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) => bail!("ping response too short"),
|
||||||
|
Ok(Err(e)) => Err(e.into()),
|
||||||
|
Err(_) => bail!("ping timed out"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+15
-1
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||||
@@ -33,6 +33,20 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
super::connect::get_status(client).await
|
super::connect::get_status(client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
color_eyre::eyre::bail!("ping not supported on mobile yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {}
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|||||||
+16
-1
@@ -8,7 +8,7 @@ use crate::app::{Command, SharedState};
|
|||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -91,9 +91,24 @@ pub trait PlatformInterface {
|
|||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
|
/// Ping a mumble server via UDP to get version, user count, etc.
|
||||||
|
fn ping_server(
|
||||||
|
address: &str,
|
||||||
|
port: u16,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
||||||
|
|
||||||
|
/// Save the default server URL.
|
||||||
|
fn set_default_server(server: &str) -> Option<()>;
|
||||||
|
|
||||||
|
/// Load the saved server list.
|
||||||
|
fn load_servers() -> Vec<ServerEntry>;
|
||||||
|
|
||||||
|
/// Save the server list.
|
||||||
|
fn save_servers(servers: &[ServerEntry]);
|
||||||
|
|
||||||
/// Async sleep for the given duration.
|
/// Async sleep for the given duration.
|
||||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -3,7 +3,7 @@
|
|||||||
use crate::{app::SharedState, effects::AudioProcessor};
|
use crate::{app::SharedState, effects::AudioProcessor};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct StubPlatform;
|
pub struct StubPlatform;
|
||||||
@@ -36,10 +36,29 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ping_server(
|
||||||
|
_address: &str,
|
||||||
|
_port: u16,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||||
|
async { panic!("stubbed platform") }
|
||||||
|
}
|
||||||
|
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-1
@@ -6,7 +6,7 @@ use dioxus::prelude::*;
|
|||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use js_sys::Float32Array;
|
use js_sys::Float32Array;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
@@ -125,6 +125,33 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
// UDP ping not available in browsers; use get_status via HTTP proxy instead
|
||||||
|
color_eyre::eyre::bail!("UDP ping not supported on web platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
.and_then(|s| s.get_item("servers").ok()?)
|
||||||
|
.and_then(|json| serde_json::from_str(&json).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
if let Some(storage) = web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
{
|
||||||
|
let _ = storage.set_item("servers", &json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn sleep(duration: Duration) {
|
async fn sleep(duration: Duration) {
|
||||||
TimeoutFuture::new(duration.as_millis() as u32).await;
|
TimeoutFuture::new(duration.as_millis() as u32).await;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user