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:
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//! 连接管理
|
||||
//! Connection management
|
||||
|
||||
pub mod client;
|
||||
pub mod resend;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//! 加密模块
|
||||
//! Cryptography module
|
||||
|
||||
pub mod eax;
|
||||
pub mod ephemeral;
|
||||
|
||||
@@ -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
@@ -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,4 +1,4 @@
|
||||
//! 网络模块
|
||||
//! Network module
|
||||
|
||||
pub mod resolver;
|
||||
pub mod socket;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//! 协议模块
|
||||
//! Protocol module
|
||||
|
||||
pub mod commands;
|
||||
pub mod packet;
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//! 数据包处理测试
|
||||
//! Packet processing tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//! 配置管理
|
||||
//! Configuration management
|
||||
|
||||
use chrono::Utc;
|
||||
use rusqlite::params;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user