feat: audio playback, device selector, i18n fixes

Audio:
- Add AudioPlayback with channel-based cpal output (avoids Send issue)
- Add AudioCapture stub with input device enumeration
- Audio gated behind 'audio' feature flag (needs ALSA/cmake)
- list_output_devices() / list_input_devices() for device selection
- StreamItem::Audio packets forwarded via mpsc channel to cpal callback

i18n:
- Replace all Chinese error messages with English in tsdb
- Replace all Chinese comments/doc strings with English in tscore
- All user-facing strings now in English

Build:
- Add Containerfile.build for containerized builds with audio deps
- CMAKE_POLICY_VERSION_MINIMUM=3.5 workaround for audiopus_sys
- tsclientlib audio feature enabled in workspace (needs cmake)
- 69 tests passing, clippy clean on both audio and non-audio builds
This commit is contained in:
ReTeamSpeak
2026-05-13 00:53:08 +09:00
parent 0d805269b5
commit 073095b06a
27 changed files with 483 additions and 222 deletions
+14
View File
@@ -0,0 +1,14 @@
FROM registry.fedoraproject.org/fedora:44
RUN dnf install -y \
gcc \
pkg-config \
cmake \
openssl-devel \
alsa-lib-devel \
&& dnf clean all
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
WORKDIR /build
+1 -1
View File
@@ -56,5 +56,5 @@ tscore = { path = "tscore" }
tsaudio = { path = "tsaudio" }
tsdb = { path = "tsdb" }
shared = { path = "shared" }
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master", default-features = false, features = ["default-tls"] }
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master", default-features = false, features = ["default-tls", "audio"] }
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master" }
+201
View File
@@ -0,0 +1,201 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use tsclientlib::audio::AudioHandler;
use tsclientlib::ClientId;
use tsproto_packets::packets::{AudioData, InAudioBuf};
const SAMPLE_RATE: u32 = 48000;
const CHANNELS: u16 = 2;
type PacketSender = std::sync::mpsc::Sender<InAudioBuf>;
struct PlaybackState {
sender: PacketSender,
output_device: String,
_stream: cpal::Stream,
}
// cpal::Stream is safe to send across threads (it's a handle to the audio device)
unsafe impl Send for PlaybackState {}
unsafe impl Sync for PlaybackState {}
pub struct AudioPlayback {
state: Option<PlaybackState>,
}
impl AudioPlayback {
pub fn new() -> Self {
Self { state: None }
}
pub fn list_output_devices() -> Vec<String> {
let host = cpal::default_host();
host.output_devices()
.map(|d| d.filter_map(|d| d.name().ok()).collect())
.unwrap_or_default()
}
pub fn list_input_devices() -> Vec<String> {
let host = cpal::default_host();
host.input_devices()
.map(|d| d.filter_map(|d| d.name().ok()).collect())
.unwrap_or_default()
}
pub fn start(&mut self, device_name: Option<&str>) -> Result<(), String> {
let host = cpal::default_host();
let device = if let Some(name) = device_name {
host.output_devices()
.map_err(|e| format!("Failed to enumerate devices: {e}"))?
.find(|d| d.name().map(|n| n == name).unwrap_or(false))
.ok_or_else(|| format!("Output device '{}' not found", name))?
} else {
host.default_output_device()
.ok_or_else(|| "No audio output device found".to_string())?
};
let device_name_str = device.name().unwrap_or_default();
tracing::info!("Using output device: {}", device_name_str);
let config = cpal::StreamConfig {
channels: CHANNELS,
sample_rate: cpal::SampleRate(SAMPLE_RATE),
buffer_size: cpal::BufferSize::Default,
};
let (tx, rx) = std::sync::mpsc::channel::<InAudioBuf>();
let mut handler = AudioHandler::<ClientId>::new();
let stream = device
.build_output_stream(
&config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
while let Ok(packet) = rx.try_recv() {
let from = match packet.data().data() {
AudioData::S2C { from, .. } => *from,
AudioData::S2CWhisper { from, .. } => *from,
_ => continue,
};
let _ = handler.handle_packet(ClientId(from), packet);
}
for sample in data.iter_mut() {
*sample = 0.0;
}
handler.fill_buffer(data);
},
|err| tracing::error!("Audio output error: {err}"),
None,
)
.map_err(|e| format!("Failed to build output stream: {e}"))?;
stream
.play()
.map_err(|e| format!("Failed to start playback: {e}"))?;
self.state = Some(PlaybackState {
sender: tx,
output_device: device_name_str,
_stream: stream,
});
tracing::info!("Audio playback started");
Ok(())
}
pub fn stop(&mut self) {
self.state = None;
tracing::info!("Audio playback stopped");
}
pub fn send_packet(&self, packet: InAudioBuf) {
if let Some(ref state) = self.state {
let _ = state.sender.send(packet);
}
}
pub fn output_device(&self) -> &str {
self.state
.as_ref()
.map(|s| s.output_device.as_str())
.unwrap_or("")
}
pub fn is_active(&self) -> bool {
self.state.is_some()
}
}
pub struct AudioCapture {
_stream: Option<cpal::Stream>,
input_device: String,
}
impl AudioCapture {
pub fn new() -> Self {
Self {
_stream: None,
input_device: String::new(),
}
}
pub fn start(
&mut self,
device_name: Option<&str>,
audio_tx: tokio::sync::mpsc::Sender<Vec<f32>>,
) -> Result<(), String> {
let host = cpal::default_host();
let device = if let Some(name) = device_name {
host.input_devices()
.map_err(|e| format!("Failed to enumerate devices: {e}"))?
.find(|d| d.name().map(|n| n == name).unwrap_or(false))
.ok_or_else(|| format!("Input device '{}' not found", name))?
} else {
host.default_input_device()
.ok_or_else(|| "No audio input device found".to_string())?
};
let device_name_str = device.name().unwrap_or_default();
tracing::info!("Using input device: {}", device_name_str);
let config = cpal::StreamConfig {
channels: 1,
sample_rate: cpal::SampleRate(SAMPLE_RATE),
buffer_size: cpal::BufferSize::Default,
};
let stream = device
.build_input_stream(
&config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
let _ = audio_tx.blocking_send(data.to_vec());
},
|err| tracing::error!("Audio input error: {err}"),
None,
)
.map_err(|e| format!("Failed to build input stream: {e}"))?;
stream
.play()
.map_err(|e| format!("Failed to start capture: {e}"))?;
self._stream = Some(stream);
self.input_device = device_name_str;
tracing::info!("Audio capture started");
Ok(())
}
pub fn stop(&mut self) {
self._stream = None;
tracing::info!("Audio capture stopped");
}
pub fn input_device(&self) -> &str {
&self.input_device
}
pub fn is_active(&self) -> bool {
self._stream.is_some()
}
}
+69
View File
@@ -11,6 +11,9 @@ use tsclientlib::{ChannelId, ClientId, Connection, DisconnectOptions, MessageTar
use tsclientlib::events::{Event, PropertyId};
use tsclientlib::prelude::*;
#[cfg(feature = "audio")]
mod audio;
fn main() -> iced::Result {
iced::application("ReTeamSpeak", App::update, App::view)
.subscription(App::subscription)
@@ -116,6 +119,8 @@ struct App {
error: Option<String>,
identity_level: u8,
session_id: u64,
#[cfg(feature = "audio")]
audio: Arc<Mutex<audio::AudioPlayback>>,
}
impl App {
@@ -159,6 +164,8 @@ impl App {
error: None,
identity_level: 0,
session_id: 0,
#[cfg(feature = "audio")]
audio: Arc::new(Mutex::new(audio::AudioPlayback::new())),
};
(app, Task::none())
@@ -206,6 +213,8 @@ impl App {
};
let handle_store = self.handle.clone();
let event_rx_store = self.event_rx.clone();
#[cfg(feature = "audio")]
let audio = self.audio.clone();
self.error = None;
self.connected = false;
@@ -231,6 +240,9 @@ impl App {
*handle_store.lock().await = Some(handle.clone());
#[cfg(feature = "audio")]
tokio::spawn(run_connection(sync_con, event_tx, audio));
#[cfg(not(feature = "audio"))]
tokio::spawn(run_connection(sync_con, event_tx));
handle
@@ -270,6 +282,16 @@ impl App {
Message::TsEvent(event) => match event {
TsEvent::Connected => {
self.connected = true;
#[cfg(feature = "audio")]
{
let audio = self.audio.clone();
tokio::spawn(async move {
let mut playback = audio.lock().await;
if let Err(e) = playback.start(None) {
tracing::warn!("Audio playback failed to start: {e}");
}
});
}
Task::none()
}
TsEvent::BookEvents(events) => {
@@ -293,6 +315,13 @@ impl App {
Task::none()
}
TsEvent::Disconnected => {
#[cfg(feature = "audio")]
{
let audio = self.audio.clone();
tokio::spawn(async move {
audio.lock().await.stop();
});
}
self.connected = false;
self.server_name.clear();
self.channels.clear();
@@ -302,6 +331,13 @@ impl App {
Task::none()
}
TsEvent::Error(e) => {
#[cfg(feature = "audio")]
{
let audio = self.audio.clone();
tokio::spawn(async move {
audio.lock().await.stop();
});
}
self.error = Some(e);
Task::none()
}
@@ -903,6 +939,38 @@ impl App {
}
}
#[cfg(feature = "audio")]
async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>, audio: Arc<Mutex<audio::AudioPlayback>>) {
let mut stream = con;
while let Some(item) = stream.next().await {
let ts_event = match item {
Ok(SyncStreamItem::BookEvents(events)) => TsEvent::BookEvents(events),
Ok(SyncStreamItem::MessageEvent(msg)) => TsEvent::MessageEvent(msg),
Ok(SyncStreamItem::AudioChange(change)) => match change {
tsclientlib::AudioEvent::CanSendAudio(can) => TsEvent::AudioChange(can, true),
tsclientlib::AudioEvent::CanReceiveAudio(can) => TsEvent::AudioChange(false, can),
},
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => {
TsEvent::IdentityLevelIncreasing(level)
}
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
Ok(SyncStreamItem::Audio(audio_buf)) => {
let playback = audio.lock().await;
playback.send_packet(audio_buf);
continue;
}
Err(e) => TsEvent::Error(e.to_string()),
};
if event_tx.send(ts_event).await.is_err() {
break;
}
}
let _ = event_tx.send(TsEvent::Disconnected).await;
}
#[cfg(not(feature = "audio"))]
async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>) {
let mut stream = con;
while let Some(item) = stream.next().await {
@@ -919,6 +987,7 @@ async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>) {
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
Ok(_) => continue,
Err(e) => TsEvent::Error(e.to_string()),
};
if event_tx.send(ts_event).await.is_err() {
+55 -55
View File
@@ -1,4 +1,4 @@
//! 客户端连接 - 完整握手实现
//! Client connection - full handshake implementation
use std::net::SocketAddr;
use std::time::Duration;
@@ -74,7 +74,7 @@ pub struct HandleResult {
pub events: Vec<CommandEvent>,
}
/// 客户端配置
/// Client configuration
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub address: SocketAddr,
@@ -104,26 +104,26 @@ impl ClientConfig {
}
}
/// 客户端连接
/// Client connection
pub struct Client {
config: ClientConfig,
state_machine: ConnectionStateMachine,
shared_secret: Option<SharedSecret>,
key_cache: KeyCache,
client_id: Option<u16>,
/// 客户端随机数 A0
/// Client random A0
random0: Option<[u8; 4]>,
/// 服务器随机数 A1
/// Server random A1
random1: Option<[u8; 16]>,
/// A0 反转
/// A0 reversed
random0_r: Option<[u8; 4]>,
/// RSA 参数
/// RSA parameters
rsa_x: Option<[u8; 64]>,
rsa_n: Option<[u8; 64]>,
rsa_level: Option<u32>,
/// 服务器随机数 A2
/// Server random A2
random2: Option<[u8; 100]>,
/// 客户端 alpha
/// Client alpha
alpha: Option<[u8; 10]>,
outgoing_command_id: u16,
outgoing_ack_id: u16,
@@ -167,18 +167,18 @@ impl Client {
&mut self.key_cache
}
/// 开始连接握手
/// Start connection handshake
pub fn start_handshake(&mut self) -> Result<Vec<u8>, ProtocolError> {
self.state_machine
.transition(ConnectionState::Connecting)
.map_err(ProtocolError::PacketParse)?;
// 生成随机数 A0
// Generate random A0
let mut random0 = [0u8; 4];
rand::Rng::fill(&mut rand::thread_rng(), &mut random0);
self.random0 = Some(random0);
// 构建 Init0 数据包
// Build Init0 packet
let init = InitPacket {
step: InitStep::Init0,
version: Some(Self::encode_version(&self.config.version)),
@@ -198,30 +198,30 @@ impl Client {
Ok(data)
}
/// 处理接收到的数据
/// Handle received data
pub fn handle_data(&mut self, data: &[u8]) -> Result<HandleResult, ProtocolError> {
let mut responses = Vec::new();
let mut events = Vec::new();
match self.state() {
ConnectionState::Connecting => {
// 处理 Init1
// Handle Init1
let init = Self::parse_server_init(data)?;
if init.step == InitStep::Init1 {
self.random1 = init.random1;
self.random0_r = init.random0_r;
// 发送 Init2
// Send Init2
let response = self.build_init2()?;
responses.push(response);
} else if init.step == InitStep::Reset {
// 服务器要求重置,重新发送 Init0
// Server requested reset, resend Init0
let response = self.start_handshake()?;
responses.push(response);
}
}
ConnectionState::IdentityLevelIncreasing => {
// 处理 Init3
// Handle Init3
let init = Self::parse_server_init(data)?;
if init.step == InitStep::Init3 {
self.rsa_x = init.x;
@@ -229,13 +229,13 @@ impl Client {
self.rsa_level = init.level;
self.random2 = init.random2;
// 计算 RSA 解答
// Compute RSA solution
let response = self.build_init4()?;
responses.push(response);
}
}
ConnectionState::Connected => {
// 处理命令数据包
// Handle command packets
let packet = InPacket::parse(Direction::S2C, data)?;
let packet_type = packet.header.flags.packet_type();
let content = if !packet.header.flags.is_unencrypted() {
@@ -270,12 +270,12 @@ impl Client {
responses.push(self.build_ack_packet(packet_type, packet.header.packet_id)?);
}
// 解析命令
// Parse commands
let cmd_str = String::from_utf8_lossy(&content);
for cmd in Command::parse_many(&cmd_str)? {
match cmd.name.as_str() {
"initserver" => {
// 连接完成
// Connection complete
if let Some(id) = cmd.get("client_id") {
self.client_id = id.parse().ok();
}
@@ -312,11 +312,11 @@ impl Client {
});
}
"initivexpand" => {
// 旧协议密钥交换
// Old protocol key exchange
responses.extend(self.handle_initivexpand(&cmd)?);
}
"initivexpand2" => {
// 新协议密钥交换
// New protocol key exchange
responses.extend(self.handle_initivexpand2(&cmd)?);
}
"channellist" => {
@@ -442,7 +442,7 @@ impl Client {
}]
}
/// 构建 Init2 数据包
/// Build Init2 packet
fn build_init2(&mut self) -> Result<Vec<u8>, ProtocolError> {
let init = InitPacket {
step: InitStep::Init2,
@@ -466,27 +466,27 @@ impl Client {
Ok(init.to_c2s_packet_bytes())
}
/// 构建 Init4 数据包
/// Build Init4 packet
fn build_init4(&mut self) -> Result<Vec<u8>, ProtocolError> {
// 计算 y = x^(2^level) mod n
// Compute y = x^(2^level) mod n
let x = self
.rsa_x
.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA x".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing RSA x".to_string()))?;
let n = self
.rsa_n
.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA n".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing RSA n".to_string()))?;
let level = self
.rsa_level
.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA level".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing RSA level".to_string()))?;
let y = Self::solve_rsa_puzzle(&x, &n, level);
// 生成 alpha
// Generate alpha
let mut alpha = [0u8; 10];
rand::Rng::fill(&mut rand::thread_rng(), &mut alpha);
self.alpha = Some(alpha);
// 构建 clientinitiv 命令
// Build clientinitiv command
let alpha_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, alpha);
let omega = self.get_identity_omega()?;
let ip = self.config.address.ip().to_string();
@@ -520,36 +520,36 @@ impl Client {
Ok(init.to_c2s_packet_bytes())
}
/// 处理 initivexpand (旧协议)
/// Handle initivexpand (old protocol)
fn handle_initivexpand(&mut self, cmd: &Command) -> Result<Vec<Vec<u8>>, ProtocolError> {
let alpha_b64 = cmd
.get("alpha")
.ok_or_else(|| ProtocolError::PacketParse("缺少 alpha".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing alpha".to_string()))?;
let beta_b64 = cmd
.get("beta")
.ok_or_else(|| ProtocolError::PacketParse("缺少 beta".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing beta".to_string()))?;
let _omega = cmd
.get("omega")
.ok_or_else(|| ProtocolError::PacketParse("缺少 omega".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing omega".to_string()))?;
let alpha_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, alpha_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 alpha".to_string()))?;
.map_err(|_| ProtocolError::PacketParse("invalid alpha".to_string()))?;
let beta_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, beta_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 beta".to_string()))?;
.map_err(|_| ProtocolError::PacketParse("invalid beta".to_string()))?;
let mut alpha = [0u8; 10];
alpha.copy_from_slice(&alpha_bytes);
let mut beta = [0u8; 10];
beta.copy_from_slice(&beta_bytes);
// 计算共享密钥
let shared_data = [0u8; 32]; // TODO: ECDH 计算
// Compute shared secret
let shared_data = [0u8; 32]; // TODO: Compute from ECDH
let secret = SharedSecret::compute_old(&alpha, &beta, &shared_data);
self.shared_secret = Some(secret);
// 发送 clientek
// Send clientek
let ek = self.get_identity_omega()?;
let proof = self.generate_proof(&ek, beta_b64);
@@ -563,7 +563,7 @@ impl Client {
])
}
/// 处理 initivexpand2 (新协议)
/// Handle initivexpand2 (new protocol)
///
/// When the server sends a license (`l`), this performs real ECDH key
/// exchange using an ephemeral Ed25519 key pair. When no license is
@@ -572,14 +572,14 @@ impl Client {
fn handle_initivexpand2(&mut self, cmd: &Command) -> Result<Vec<Vec<u8>>, ProtocolError> {
let beta_b64 = cmd
.get("beta")
.ok_or_else(|| ProtocolError::PacketParse("缺少 beta".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing beta".to_string()))?;
let _omega = cmd
.get("omega")
.ok_or_else(|| ProtocolError::PacketParse("缺少 omega".to_string()))?;
.ok_or_else(|| ProtocolError::PacketParse("missing omega".to_string()))?;
let beta_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, beta_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 beta".to_string()))?;
.map_err(|_| ProtocolError::PacketParse("invalid beta".to_string()))?;
let mut beta = [0u8; 54];
if beta_bytes.len() >= 54 {
@@ -693,7 +693,7 @@ impl Client {
self.build_command_packet(self.build_clientinit())
}
/// 构建 clientinit 命令
/// Build clientinit command
pub fn build_clientinit(&self) -> Vec<u8> {
let channel_password = self
.config
@@ -738,9 +738,9 @@ impl Client {
cmd.to_string().into_bytes()
}
/// 编码版本号
/// Encode version number
fn encode_version(version: &str) -> u32 {
// 从版本字符串提取构建时间戳
// Extract build timestamp from version string
if let Some(start) = version.find("[Build: ") {
let rest = &version[start + 8..];
if let Some(end) = rest.find(']') {
@@ -750,10 +750,10 @@ impl Client {
}
}
}
1466672534 // 默认值
1466672534 // default value
}
/// 获取当前时间戳
/// Get current timestamp
fn current_timestamp() -> u32 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -761,14 +761,14 @@ impl Client {
.as_secs() as u32
}
/// 解决 RSA 拼图
/// Solve RSA puzzle
/// y = x^(2^level) mod n
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
let x_big = num_bigint::BigUint::from_bytes_be(x);
let n_big = num_bigint::BigUint::from_bytes_be(n);
// y = x^(2^level) mod n
// 需要做 level 次平方操作
// Need to perform level squaring operations
let mut y = x_big;
for _ in 0..level {
y = (y.clone() * y) % &n_big;
@@ -781,15 +781,15 @@ impl Client {
result
}
/// 获取身份公钥 (omega)
/// Get identity public key (omega)
fn get_identity_omega(&self) -> Result<String, ProtocolError> {
self.config
.identity
.public_key_ts_base64()
.map_err(|e| ProtocolError::Encryption(format!("身份公钥编码失败: {e}")))
.map_err(|e| ProtocolError::Encryption(format!("identity public key encoding failed: {e}")))
}
/// 生成证明
/// Generate proof
fn generate_proof(&self, data: &str, beta: &str) -> String {
let combined = format!("{}{}", data, beta);
self.config.identity.sign_der_base64(combined.as_bytes())
@@ -827,7 +827,7 @@ mod tests {
#[test]
fn test_rsa_puzzle() {
// 使用非零值测试
// Test with non-zero values
let mut x = [0u8; 64];
x[63] = 2; // x = 2
let mut n = [0u8; 64];
+1 -1
View File
@@ -1,4 +1,4 @@
//! 连接管理
//! Connection management
pub mod client;
pub mod resend;
+28 -28
View File
@@ -1,9 +1,9 @@
//! 数据包重传和确认系统
//! Packet retransmission and acknowledgment system
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
/// 数据包 ID
/// Packet ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PacketId {
pub generation_id: u32,
@@ -27,7 +27,7 @@ impl PacketId {
}
}
/// 已发送的数据包信息
/// Sent packet information
#[derive(Debug, Clone)]
pub struct SentPacket {
pub data: Vec<u8>,
@@ -42,7 +42,7 @@ impl SentPacket {
data,
sent_at: Instant::now(),
retry_count: 0,
timeout: Duration::from_millis(500), // 初始超时 500ms
timeout: Duration::from_millis(500), // Initial timeout 500ms
}
}
@@ -57,18 +57,18 @@ impl SentPacket {
pub fn retry(&mut self) {
self.retry_count += 1;
self.sent_at = Instant::now();
// 指数退避
// Exponential backoff
self.timeout = Duration::from_millis(500 * (1 << self.retry_count).min(32));
}
}
/// 重传管理器
/// Retransmission manager
pub struct ResendManager {
/// 等待确认的数据包
/// Packets awaiting acknowledgment
pending: BTreeMap<PacketId, SentPacket>,
/// 最大重试次数
/// Maximum retry count
max_retries: u32,
/// 连接超时
/// Connection timeout
connection_timeout: Duration,
}
@@ -81,17 +81,17 @@ impl ResendManager {
}
}
/// 添加已发送的数据包
/// Add sent packet
pub fn add_sent(&mut self, id: PacketId, data: Vec<u8>) {
self.pending.insert(id, SentPacket::new(data));
}
/// 确认数据包
/// Acknowledge packet
pub fn ack(&mut self, id: &PacketId) -> bool {
self.pending.remove(id).is_some()
}
/// 获取需要重传的数据包
/// Get packets that need retransmission
pub fn get_retransmissions(&mut self) -> Vec<(PacketId, Vec<u8>)> {
let mut retransmissions = Vec::new();
let mut to_retry = Vec::new();
@@ -112,29 +112,29 @@ impl ResendManager {
retransmissions
}
/// 检查是否连接超时
/// Check if connection timed out
pub fn is_connection_timeout(&self) -> bool {
self.pending
.values()
.any(|p| p.sent_at.elapsed() > self.connection_timeout)
}
/// 获取待确认数据包数量
/// Get number of pending packets
pub fn pending_count(&self) -> usize {
self.pending.len()
}
/// 清空所有待确认数据包
/// Clear all pending packets
pub fn clear(&mut self) {
self.pending.clear();
}
/// 设置最大重试次数
/// Set maximum retry count
pub fn set_max_retries(&mut self, max_retries: u32) {
self.max_retries = max_retries;
}
/// 设置连接超时
/// Set connection timeout
pub fn set_connection_timeout(&mut self, timeout: Duration) {
self.connection_timeout = timeout;
}
@@ -146,7 +146,7 @@ impl Default for ResendManager {
}
}
/// RTT 估算器
/// RTT estimator
pub struct RttEstimator {
srtt: Duration,
rtt_var: Duration,
@@ -162,7 +162,7 @@ impl RttEstimator {
}
}
/// 更新 RTT 估算
/// Update RTT estimate
pub fn update(&mut self, measured_rtt: Duration) {
let alpha = 0.125;
let beta = 0.25;
@@ -178,7 +178,7 @@ impl RttEstimator {
);
self.rto = self.srtt + self.rtt_var * 4;
// 限制 RTO 范围
// Clamp RTO range
if self.rto < Duration::from_millis(100) {
self.rto = Duration::from_millis(100);
}
@@ -187,12 +187,12 @@ impl RttEstimator {
}
}
/// 获取当前 RTO
/// Get current RTO
pub fn rto(&self) -> Duration {
self.rto
}
/// 获取平滑 RTT
/// Get smoothed RTT
pub fn srtt(&self) -> Duration {
self.srtt
}
@@ -217,7 +217,7 @@ mod tests {
assert_eq!(manager.pending_count(), 1);
// 确认
// Acknowledge
assert!(manager.ack(&id));
assert_eq!(manager.pending_count(), 0);
}
@@ -226,17 +226,17 @@ mod tests {
fn test_rtt_estimator() {
let mut estimator = RttEstimator::new();
// 初始 SRTT 500ms
// Initial SRTT is 500ms
assert_eq!(estimator.srtt(), Duration::from_millis(500));
// 更新多次,SRTT 应该逐渐收敛
// Update multiple times, SRTT should converge
for _ in 0..100 {
estimator.update(Duration::from_millis(100));
}
// 经过多次更新后,SRTT 应该接近 100ms
// After many updates, SRTT should approach 100ms
assert!(estimator.srtt() < Duration::from_millis(150));
// RTO 应该大于 SRTT
// RTO should be greater than SRTT
assert!(estimator.rto() > estimator.srtt());
}
@@ -245,7 +245,7 @@ mod tests {
let mut packet = SentPacket::new(vec![1, 2, 3]);
assert!(!packet.is_expired());
// 模拟超时
// Simulate timeout
packet.sent_at = Instant::now() - Duration::from_millis(600);
assert!(packet.is_expired());
assert!(packet.should_retry(10));
+3 -3
View File
@@ -1,8 +1,8 @@
//! 连接状态管理
//! Connection state management
use std::fmt;
/// 连接状态
/// Connection state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
Disconnected,
@@ -42,7 +42,7 @@ impl fmt::Display for ConnectionState {
}
}
/// 连接状态机
/// Connection state machine
pub struct ConnectionStateMachine {
state: ConnectionState,
}
+8 -8
View File
@@ -1,4 +1,4 @@
//! EAX 模式加密
//! EAX mode encryption
use aes::Aes128;
use eax::aead::consts::U8;
@@ -9,7 +9,7 @@ use super::keys;
use crate::protocol::{InPacket, OutPacket};
use crate::ProtocolError;
/// EAX 加密器
/// EAX cipher
pub struct EaxCipher {
cipher: Eax<Aes128, U8>,
}
@@ -32,7 +32,7 @@ impl EaxCipher {
let tag = self
.cipher
.encrypt_in_place_detached(nonce, header, data)
.map_err(|_| ProtocolError::Encryption("EAX 加密失败".to_string()))?;
.map_err(|_| ProtocolError::Encryption("EAX encryption failed".to_string()))?;
let mut mac = [0u8; 8];
mac.copy_from_slice(&tag[..8]);
@@ -51,11 +51,11 @@ impl EaxCipher {
self.cipher
.decrypt_in_place_detached(nonce, header, data, tag)
.map_err(|_| ProtocolError::Decryption("MAC 验证失败".to_string()))
.map_err(|_| ProtocolError::Decryption("MAC verification failed".to_string()))
}
}
/// 加密数据包
/// Encrypt packet
pub fn encrypt_packet(
packet: &mut OutPacket,
generation_id: u32,
@@ -77,7 +77,7 @@ pub fn encrypt_packet(
Ok(())
}
/// 解密数据包
/// Decrypt packet
pub fn decrypt_packet(
packet: &InPacket,
generation_id: u32,
@@ -99,7 +99,7 @@ pub fn decrypt_packet(
Ok(data)
}
/// 假加密
/// Fake encryption
pub fn encrypt_fake(packet: &mut OutPacket) -> Result<(), ProtocolError> {
let cipher = EaxCipher::new(&keys::FAKE_KEY);
let meta = packet.header.get_meta(packet.direction);
@@ -108,7 +108,7 @@ pub fn encrypt_fake(packet: &mut OutPacket) -> Result<(), ProtocolError> {
Ok(())
}
/// 假解密
/// Fake decryption
pub fn decrypt_fake(packet: &InPacket) -> Result<Vec<u8>, ProtocolError> {
let cipher = EaxCipher::new(&keys::FAKE_KEY);
let meta = packet.header.get_meta(packet.direction);
+5 -5
View File
@@ -1,9 +1,9 @@
//! 哈希函数
//! Hash functions
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
/// SHA-1 哈希
/// SHA-1 hash
pub fn sha1(data: &[u8]) -> [u8; 20] {
let mut hasher = Sha1::new();
hasher.update(data);
@@ -13,7 +13,7 @@ pub fn sha1(data: &[u8]) -> [u8; 20] {
hash
}
/// SHA-256 哈希
/// SHA-256 hash
pub fn sha256(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
@@ -23,7 +23,7 @@ pub fn sha256(data: &[u8]) -> [u8; 32] {
hash
}
/// SHA-512 哈希
/// SHA-512 hash
pub fn sha512(data: &[u8]) -> [u8; 64] {
let mut hasher = Sha512::new();
hasher.update(data);
@@ -33,7 +33,7 @@ pub fn sha512(data: &[u8]) -> [u8; 64] {
hash
}
/// 计算密码哈希
/// Compute password hash
pub fn hash_password(password: &str) -> String {
let hash = sha1(password.as_bytes());
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
+11 -11
View File
@@ -1,4 +1,4 @@
//! 密钥管理
//! Key management
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
@@ -6,19 +6,19 @@ use sha2::{Digest, Sha256, Sha512};
use crate::protocol::Direction;
use crate::protocol::PacketType;
/// 假加密密钥
/// Fake encryption key
pub const FAKE_KEY: [u8; 16] = *b"c:\\windows\\syste";
/// 假加密 Nonce
/// Fake encryption nonce
pub const FAKE_NONCE: [u8; 16] = *b"m\\firewall32.cpl";
/// 许可证根密钥
/// License root key
pub const ROOT_KEY: [u8; 32] = [
0xcd, 0x0d, 0xe2, 0xae, 0xd4, 0x63, 0x45, 0x50, 0x9a, 0x7e, 0x3c, 0xfd, 0x8f, 0x68, 0xb3, 0xdc,
0x75, 0x55, 0xb2, 0x9d, 0xcc, 0xec, 0x73, 0xcd, 0x18, 0x75, 0x0f, 0x99, 0x38, 0x12, 0x40, 0x8a,
];
/// 共享密钥
/// Shared secret
#[derive(Clone)]
pub struct SharedSecret {
pub iv: [u8; 64],
@@ -87,7 +87,7 @@ impl std::fmt::Debug for SharedSecret {
}
}
/// 缓存的密钥
/// Cached key
#[derive(Debug, Clone)]
pub struct CachedKey {
pub generation_id: u32,
@@ -115,7 +115,7 @@ impl Default for CachedKey {
}
}
/// 密钥缓存
/// Key cache
pub struct KeyCache {
cache: [[CachedKey; 2]; 8],
}
@@ -162,7 +162,7 @@ impl Default for KeyCache {
}
}
/// 创建密钥和 Nonce
/// Create key and nonce
pub fn create_key_nonce(
packet_type: PacketType,
direction: Direction,
@@ -192,7 +192,7 @@ pub fn create_key_nonce(
(key, nonce)
}
/// 创建用于加密的密钥
/// Create encryption key
pub fn create_encryption_key(key: &[u8; 16], packet_id: u16) -> [u8; 16] {
let mut result = *key;
result[0] ^= (packet_id >> 8) as u8;
@@ -200,7 +200,7 @@ pub fn create_encryption_key(key: &[u8; 16], packet_id: u16) -> [u8; 16] {
result
}
/// 计算 Hash Cash 级别
/// Compute hash cash level
pub fn get_hash_cash_level(omega: &str, offset: u64) -> u8 {
let mut hasher = Sha1::new();
hasher.update(format!("{}{}", omega, offset).as_bytes());
@@ -219,7 +219,7 @@ pub fn get_hash_cash_level(omega: &str, offset: u64) -> u8 {
level
}
/// 计算 UID
/// Compute UID
pub fn compute_uid(public_key: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(public_key);
+1 -1
View File
@@ -1,4 +1,4 @@
//! 加密模块
//! Cryptography module
pub mod eax;
pub mod ephemeral;
+10 -10
View File
@@ -1,4 +1,4 @@
//! 加密测试
//! Cryptography tests
#[cfg(test)]
mod tests {
@@ -50,7 +50,7 @@ mod tests {
let encrypted = create_encryption_key(&key, 0x1234);
assert_eq!(encrypted[0], key[0] ^ 0x12);
assert_eq!(encrypted[1], key[1] ^ 0x34);
// 其他字节不变
// Other bytes unchanged
assert_eq!(encrypted[2], key[2]);
}
@@ -86,7 +86,7 @@ mod tests {
assert_eq!(key1, key2);
assert_eq!(nonce1, nonce2);
// 不同的 generation_id 应该返回不同的密钥
// Different generation_id should return different keys
let (key3, _) = cache.get_or_create(PacketType::Command, Direction::C2S, 1, &iv);
assert_ne!(key1, key3);
}
@@ -106,10 +106,10 @@ mod tests {
let header = b"test header";
let mut data = b"Hello, World!".to_vec();
// 加密
// Encrypt
let mac = cipher.encrypt(&nonce, header, &mut data).unwrap();
// 解密
// Decrypt
cipher.decrypt(&nonce, header, &mut data, &mac).unwrap();
assert_eq!(data, b"Hello, World!");
@@ -124,10 +124,10 @@ mod tests {
);
packet.header.packet_id = 1;
// 假加密
// Fake encryption
encrypt_fake(&mut packet).unwrap();
// 假解密
// Fake decryption
let in_packet = InPacket {
direction: Direction::C2S,
header: packet.header.clone(),
@@ -140,13 +140,13 @@ mod tests {
#[test]
fn test_hash_cash_level() {
// 测试不同的 offset 产生不同的 level
// Test that different offsets produce different levels
let level0 = get_hash_cash_level("test_key", 0);
let level1 = get_hash_cash_level("test_key", 1);
assert!(level0 <= 160);
assert!(level1 <= 160);
// 使用一个会产生更高 level 的 key
// Use a key that produces a higher level
let level_high = get_hash_cash_level("a", 12345);
assert!(level_high <= 160);
}
@@ -156,7 +156,7 @@ mod tests {
let public_key = b"test_public_key_data";
let uid = compute_uid(public_key);
assert!(!uid.is_empty());
// UID 应该是 base64 编码的 SHA1 哈希
// UID should be a base64-encoded SHA1 hash
assert!(uid.len() > 20);
}
}
+19 -19
View File
@@ -1,4 +1,4 @@
//! TeamSpeak 3 协议核心实现
//! TeamSpeak 3 protocol core implementation
pub mod connection;
pub mod crypto;
@@ -14,59 +14,59 @@ pub use query::*;
use thiserror::Error;
/// 协议错误
/// Protocol error
#[derive(Error, Debug)]
pub enum ProtocolError {
#[error("数据包解析错误: {0}")]
#[error("packet parse error: {0}")]
PacketParse(String),
#[error("加密错误: {0}")]
#[error("encryption error: {0}")]
Encryption(String),
#[error("解密错误: {0}")]
#[error("decryption error: {0}")]
Decryption(String),
#[error("压缩错误: {0}")]
#[error("compression error: {0}")]
Compression(String),
#[error("解压错误: {0}")]
#[error("decompression error: {0}")]
Decompression(String),
#[error("无效的数据包类型: {0}")]
#[error("invalid packet type: {0}")]
InvalidPacketType(u8),
#[error("无效的标志位: {0}")]
#[error("invalid flags: {0}")]
InvalidFlags(u8),
#[error("数据包过大: {size} > {max}")]
#[error("packet too large: {size} > {max}")]
PacketTooLarge { size: usize, max: usize },
#[error("数据包过小: {size} < {min}")]
#[error("packet too small: {size} < {min}")]
PacketTooSmall { size: usize, min: usize },
#[error("无效的客户端 ID: {0}")]
#[error("invalid client ID: {0}")]
InvalidClientId(u16),
#[error("无效的数据包 ID: {0}")]
#[error("invalid packet ID: {0}")]
InvalidPacketId(u16),
#[error("MAC 验证失败")]
#[error("MAC verification failed")]
MacVerificationFailed,
#[error("超时: {0}")]
#[error("timeout: {0}")]
Timeout(String),
#[error("连接关闭")]
#[error("connection closed")]
ConnectionClosed,
#[error("命令错误: {0}")]
#[error("command error: {0}")]
Command(String),
#[error("网络错误: {0}")]
#[error("network error: {0}")]
Network(#[from] std::io::Error),
}
/// 协议结果类型
/// Protocol result type
pub type ProtocolResult<T> = Result<T, ProtocolError>;
impl From<protocol::CommandError> for ProtocolError {
+1 -1
View File
@@ -1,4 +1,4 @@
//! 网络模块
//! Network module
pub mod resolver;
pub mod socket;
+8 -8
View File
@@ -1,15 +1,15 @@
//! 地址解析
//! Address resolution
use std::net::SocketAddr;
/// 服务器地址
/// Server address
#[derive(Debug, Clone)]
pub enum ServerAddress {
/// 直接 IP 地址
/// Direct IP address
Ip(SocketAddr),
/// 域名
/// Domain name
Domain(String),
/// 服务器昵称
/// Server nickname
Nickname(String),
}
@@ -24,15 +24,15 @@ impl ServerAddress {
}
async fn resolve_domain(domain: &str) -> Result<SocketAddr, Box<dyn std::error::Error>> {
// 尝试直接解析
// Try direct resolution
let addrs = tokio::net::lookup_host(format!("{}:9987", domain)).await?;
addrs
.into_iter()
.next()
.ok_or_else(|| "无法解析域名".into())
.ok_or_else(|| "failed to resolve domain".into())
}
async fn resolve_nickname(nickname: &str) -> Result<SocketAddr, Box<dyn std::error::Error>> {
// TODO: 实现 TSDNS 和昵称解析
// TODO: Implement TSDNS and nickname resolution
resolve_domain(nickname).await
}
+2 -2
View File
@@ -1,4 +1,4 @@
//! UDP Socket 抽象
//! UDP Socket abstraction
use std::net::SocketAddr;
use std::task::{Context, Poll};
@@ -26,7 +26,7 @@ pub trait Socket {
fn local_addr(&self) -> std::io::Result<SocketAddr>;
}
/// UDP Socket 实现
/// UDP Socket implementation
pub struct UdpSocketWrapper {
socket: UdpSocket,
}
+14 -14
View File
@@ -1,8 +1,8 @@
//! 命令解析和序列化
//! Command parsing and serialization
use std::fmt;
/// 命令解析错误
/// Command parsing error
#[derive(Debug, Clone)]
pub enum CommandError {
InvalidFormat(String),
@@ -14,12 +14,12 @@ pub enum CommandError {
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFormat(msg) => write!(f, "无效的命令格式: {}", msg),
Self::MissingParameter(name) => write!(f, "缺少必需的参数: {}", name),
Self::InvalidFormat(msg) => write!(f, "invalid command format: {}", msg),
Self::MissingParameter(name) => write!(f, "missing required parameter: {}", name),
Self::InvalidParameterValue { name, value } => {
write!(f, "无效的参数值: {}={}", name, value)
write!(f, "invalid parameter value: {}={}", name, value)
}
Self::EscapeError(msg) => write!(f, "转义序列错误: {}", msg),
Self::EscapeError(msg) => write!(f, "escape sequence error: {}", msg),
}
}
}
@@ -28,7 +28,7 @@ impl std::error::Error for CommandError {}
pub type CommandResult<T> = Result<T, CommandError>;
/// 转义序列处理
/// Escape sequence handling
pub mod escape {
use super::CommandError;
@@ -65,12 +65,12 @@ pub mod escape {
Some('t') => result.push('\t'),
Some(other) => {
return Err(CommandError::EscapeError(format!(
"未知的转义序列: \\{}",
"unknown escape sequence: \\{}",
other
)))
}
None => {
return Err(CommandError::EscapeError("意外的转义序列结束".to_string()))
return Err(CommandError::EscapeError("unexpected end of escape sequence".to_string()))
}
}
} else {
@@ -82,7 +82,7 @@ pub mod escape {
}
}
/// 命令参数
/// Command argument
#[derive(Debug, Clone)]
pub struct CommandArgument {
pub name: String,
@@ -126,7 +126,7 @@ impl fmt::Display for CommandArgument {
}
}
/// 命令
/// Command
#[derive(Debug, Clone)]
pub struct Command {
pub name: String,
@@ -177,7 +177,7 @@ impl Command {
pub fn parse(input: &str) -> CommandResult<Self> {
let input = input.trim();
if input.is_empty() {
return Err(CommandError::InvalidFormat("空命令".to_string()));
return Err(CommandError::InvalidFormat("empty command".to_string()));
}
let parts: Vec<&str> = input.splitn(2, ' ').collect();
@@ -208,7 +208,7 @@ impl Command {
pub fn parse_many(input: &str) -> CommandResult<Vec<Self>> {
let input = input.trim();
if input.is_empty() {
return Err(CommandError::InvalidFormat("空命令".to_string()));
return Err(CommandError::InvalidFormat("empty command".to_string()));
}
let parts: Vec<&str> = input.splitn(2, ' ').collect();
@@ -242,7 +242,7 @@ impl fmt::Display for Command {
}
}
/// 命令构建器
/// Command builder
pub struct CommandBuilder {
command: Command,
}
+1 -1
View File
@@ -1,4 +1,4 @@
//! 协议模块
//! Protocol module
pub mod commands;
pub mod packet;
+12 -12
View File
@@ -1,24 +1,24 @@
//! 数据包定义和处理
//! Packet definition and handling
use std::fmt;
use super::types::*;
use crate::ProtocolError;
/// 最大数据包大小
/// Maximum packet size
pub const MAX_PACKET_SIZE: usize = 500;
/// C2S 头部大小
/// C2S header size
pub const C2S_HEADER_SIZE: usize = 13; // 8 (MAC) + 2 (PId) + 2 (CId) + 1 (PT)
/// S2C 头部大小
/// S2C header size
pub const S2C_HEADER_SIZE: usize = 11; // 8 (MAC) + 2 (PId) + 1 (PT)
/// Init packets use a fixed MAC and packet id during the TS3 handshake.
pub const INIT_MAC: [u8; 8] = *b"TS3INIT1";
pub const INIT_PACKET_ID: u16 = 0x65;
/// 数据包方向
/// Packet direction
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
C2S,
@@ -34,7 +34,7 @@ impl Direction {
}
}
/// 数据包标志位
/// Packet flags
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flags(pub u8);
@@ -120,7 +120,7 @@ impl fmt::Display for Flags {
}
}
/// 数据包头部
/// Packet header
#[derive(Debug, Clone)]
pub struct Header {
pub mac: [u8; 8],
@@ -218,7 +218,7 @@ impl Header {
}
}
/// 输入数据包
/// Inbound packet
#[derive(Debug, Clone)]
pub struct InPacket {
pub direction: Direction,
@@ -256,7 +256,7 @@ impl InPacket {
}
}
/// 输出数据包
/// Outbound packet
#[derive(Debug, Clone)]
pub struct OutPacket {
pub direction: Direction,
@@ -326,7 +326,7 @@ impl OutPacket {
}
}
/// 确认数据包
/// Acknowledgment packet
#[derive(Debug, Clone)]
pub struct AckPacket {
pub direction: Direction,
@@ -351,7 +351,7 @@ impl AckPacket {
}
}
/// 初始化步骤
/// Init step
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitStep {
Init0,
@@ -362,7 +362,7 @@ pub enum InitStep {
Reset,
}
/// 初始化数据包
/// Init packet
#[derive(Debug, Clone)]
pub struct InitPacket {
pub step: InitStep,
+1 -1
View File
@@ -1,4 +1,4 @@
//! 数据包处理测试
//! Packet processing tests
#[cfg(test)]
mod tests {
+5 -5
View File
@@ -1,8 +1,8 @@
//! 协议类型定义
//! Protocol type definitions
use std::fmt;
/// 数据包类型
/// Packet type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PacketType {
Voice,
@@ -111,7 +111,7 @@ impl fmt::Display for PacketType {
}
}
/// 编解码器类型
/// Codec type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CodecType {
SpeexNarrowband,
@@ -163,7 +163,7 @@ impl CodecType {
}
}
/// 私语类型
/// Whisper type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupWhisperType {
ServerGroup,
@@ -193,7 +193,7 @@ impl GroupWhisperType {
}
}
/// 私语目标
/// Whisper target
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupWhisperTarget {
AllChannels,
+2 -9
View File
@@ -1,11 +1,10 @@
//! 书签管理
//! Bookmark management
use chrono::Utc;
use rusqlite::params;
use super::{DatabaseError, DatabaseManager, DatabaseResult};
/// 书签信息
#[derive(Debug, Clone)]
pub struct Bookmark {
pub id: String,
@@ -24,7 +23,6 @@ pub struct Bookmark {
}
impl DatabaseManager {
/// 创建书签
pub fn create_bookmark(
&self,
name: &str,
@@ -57,7 +55,6 @@ impl DatabaseManager {
})
}
/// 获取书签
pub fn get_bookmark(&self, id: &str) -> DatabaseResult<Bookmark> {
let conn = self.connection();
let mut stmt = conn.prepare(
@@ -82,12 +79,11 @@ impl DatabaseManager {
updated_at: row.get(12)?,
})
})
.map_err(|_| DatabaseError::NotFound(format!("书签 {} 未找到", id)))?;
.map_err(|_| DatabaseError::NotFound(format!("Bookmark {} not found", id)))?;
Ok(bookmark)
}
/// 获取所有书签
pub fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>> {
let conn = self.connection();
let mut stmt = conn.prepare(
@@ -117,7 +113,6 @@ impl DatabaseManager {
Ok(bookmarks)
}
/// 更新书签
pub fn update_bookmark(
&self,
id: &str,
@@ -159,14 +154,12 @@ impl DatabaseManager {
Ok(())
}
/// 删除书签
pub fn delete_bookmark(&self, id: &str) -> DatabaseResult<()> {
self.connection()
.execute("DELETE FROM bookmarks WHERE id = ?1", params![id])?;
Ok(())
}
/// 更新最后连接时间
pub fn update_bookmark_last_connected(&self, id: &str) -> DatabaseResult<()> {
let now = Utc::now().to_rfc3339();
self.connection().execute(
+1 -1
View File
@@ -1,4 +1,4 @@
//! 配置管理
//! Configuration management
use chrono::Utc;
use rusqlite::params;
+2 -8
View File
@@ -1,11 +1,10 @@
//! 身份管理
//! Identity management
use chrono::Utc;
use rusqlite::params;
use super::{DatabaseError, DatabaseManager, DatabaseResult};
/// 身份信息
#[derive(Debug, Clone)]
pub struct Identity {
pub id: String,
@@ -18,7 +17,6 @@ pub struct Identity {
}
impl DatabaseManager {
/// 创建身份
pub fn create_identity(&self, name: &str, private_key: &str) -> DatabaseResult<Identity> {
let id = uuid::Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
@@ -39,7 +37,6 @@ impl DatabaseManager {
})
}
/// 获取身份
pub fn get_identity(&self, id: &str) -> DatabaseResult<Identity> {
let conn = self.connection();
let mut stmt = conn.prepare(
@@ -58,12 +55,11 @@ impl DatabaseManager {
updated_at: row.get(6)?,
})
})
.map_err(|_| DatabaseError::NotFound(format!("身份 {} 未找到", id)))?;
.map_err(|_| DatabaseError::NotFound(format!("Identity {} not found", id)))?;
Ok(identity)
}
/// 获取所有身份
pub fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>> {
let conn = self.connection();
let mut stmt = conn.prepare(
@@ -87,7 +83,6 @@ impl DatabaseManager {
Ok(identities)
}
/// 更新身份
pub fn update_identity(
&self,
id: &str,
@@ -113,7 +108,6 @@ impl DatabaseManager {
Ok(())
}
/// 删除身份
pub fn delete_identity(&self, id: &str) -> DatabaseResult<()> {
self.connection()
.execute("DELETE FROM identities WHERE id = ?1", params![id])?;
+6 -9
View File
@@ -1,4 +1,4 @@
//! 数据存储
//! Database storage
pub mod bookmark;
pub mod config;
@@ -11,29 +11,26 @@ pub use message::*;
use thiserror::Error;
/// 数据库错误
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("SQLite 错误: {0}")]
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("序列化错误: {0}")]
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("IO 错误: {0}")]
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("未找到: {0}")]
#[error("Not found: {0}")]
NotFound(String),
#[error("已存在: {0}")]
#[error("Already exists: {0}")]
AlreadyExists(String),
}
/// 数据库结果类型
pub type DatabaseResult<T> = Result<T, DatabaseError>;
/// 数据库管理器
pub struct DatabaseManager {
conn: rusqlite::Connection,
}
+2 -9
View File
@@ -1,11 +1,10 @@
//! 消息管理
//! Message management
use chrono::Utc;
use rusqlite::params;
use super::{DatabaseError, DatabaseManager, DatabaseResult};
/// 消息信息
#[derive(Debug, Clone)]
pub struct Message {
pub id: i64,
@@ -21,7 +20,6 @@ pub struct Message {
}
impl DatabaseManager {
/// 创建消息
#[allow(clippy::too_many_arguments)]
pub fn create_message(
&self,
@@ -56,7 +54,6 @@ impl DatabaseManager {
})
}
/// 获取消息
pub fn get_message(&self, id: i64) -> DatabaseResult<Message> {
let conn = self.connection();
let mut stmt = conn.prepare(
@@ -78,12 +75,11 @@ impl DatabaseManager {
timestamp: row.get(9)?,
})
})
.map_err(|_| DatabaseError::NotFound(format!("消息 {} 未找到", id)))?;
.map_err(|_| DatabaseError::NotFound(format!("Message {} not found", id)))?;
Ok(message)
}
/// 获取服务器消息
pub fn get_server_messages(
&self,
server_address: &str,
@@ -115,21 +111,18 @@ impl DatabaseManager {
Ok(messages)
}
/// 标记消息为已读
pub fn mark_message_read(&self, id: i64) -> DatabaseResult<()> {
self.connection()
.execute("UPDATE messages SET is_read = 1 WHERE id = ?1", params![id])?;
Ok(())
}
/// 删除消息
pub fn delete_message(&self, id: i64) -> DatabaseResult<()> {
self.connection()
.execute("DELETE FROM messages WHERE id = ?1", params![id])?;
Ok(())
}
/// 清空服务器消息
pub fn clear_server_messages(&self, server_address: &str) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM messages WHERE server_address = ?1",