feat: implement TS3 protocol layer, session management, and ServerQuery client
CI/CD / Test (ubuntu-latest) (push) Successful in 2m3s
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Successful in 2m3s
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
- Add real X25519 ECDH key exchange for initivexpand2 bootstrap - Add P-256 identity key generation with TeamSpeak tomcrypt format - Add session event loop with tokio::select! for packet/command handling - Add ServerQuery TCP client with typed parsing (channels, clients, permissions) - Add Tauri commands: connect (session-based), join_channel, send_message, disconnect - Add frontend ServerQuery snapshot panel - Fix CI: Node 22, rustup setup, npm install, workspace checks - Add mock UDP handshake tests through initserver - 54 tests passing across shared, tscore, tsaudio, tsdb
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface Identity {
|
||||
@@ -18,6 +18,33 @@ interface Bookmark {
|
||||
last_connected: string | null;
|
||||
}
|
||||
|
||||
interface ServerQueryChannel {
|
||||
id: number;
|
||||
name: string;
|
||||
total_clients: number;
|
||||
}
|
||||
|
||||
interface ServerQueryClient {
|
||||
id: number;
|
||||
database_id: number;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
interface ServerQueryServerInfo {
|
||||
name: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
max_clients: number;
|
||||
clients_online: number;
|
||||
}
|
||||
|
||||
interface ServerQuerySnapshot {
|
||||
server: ServerQueryServerInfo | null;
|
||||
channels: ServerQueryChannel[];
|
||||
clients: ServerQueryClient[];
|
||||
permissions: unknown[];
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [identities, setIdentities] = useState<Identity[]>([]);
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
||||
@@ -25,12 +52,21 @@ function App() {
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [queryPort, setQueryPort] = useState(10011);
|
||||
const [querySnapshot, setQuerySnapshot] = useState<ServerQuerySnapshot | null>(null);
|
||||
const [queryLoading, setQueryLoading] = useState(false);
|
||||
const [queryError, setQueryError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadIdentities();
|
||||
loadBookmarks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setQuerySnapshot(null);
|
||||
setQueryError(null);
|
||||
}, [selectedBookmark]);
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
const result = await invoke<Identity[]>('get_identities');
|
||||
@@ -74,6 +110,31 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadServerQuery() {
|
||||
if (!selectedBookmark || queryLoading) return;
|
||||
|
||||
setQueryLoading(true);
|
||||
setQueryError(null);
|
||||
try {
|
||||
const snapshot = await invoke<ServerQuerySnapshot>('server_query_snapshot', {
|
||||
request: {
|
||||
address: selectedBookmark.address,
|
||||
port: queryPort,
|
||||
username: null,
|
||||
password: null,
|
||||
virtual_server_id: null,
|
||||
include_permissions: false,
|
||||
},
|
||||
});
|
||||
setQuerySnapshot(snapshot);
|
||||
} catch (error) {
|
||||
setQueryError(String(error));
|
||||
setQuerySnapshot(null);
|
||||
} finally {
|
||||
setQueryLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
@@ -91,6 +152,7 @@ function App() {
|
||||
<aside className="sidebar">
|
||||
<section className="bookmarks-section">
|
||||
<h2>服务器书签</h2>
|
||||
<div className="identity-summary">身份数量:{identities.length}</div>
|
||||
<ul className="bookmark-list">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<li
|
||||
@@ -108,45 +170,109 @@ function App() {
|
||||
|
||||
<div className="content">
|
||||
{selectedBookmark ? (
|
||||
<div className="connect-form">
|
||||
<h2>连接到 {selectedBookmark.name}</h2>
|
||||
<div className="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={`${selectedBookmark.address}:${selectedBookmark.port}`}
|
||||
disabled
|
||||
/>
|
||||
<div className="server-panel">
|
||||
<div className="connect-form">
|
||||
<h2>连接到 {selectedBookmark.name}</h2>
|
||||
<div className="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={`${selectedBookmark.address}:${selectedBookmark.port}`}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder={selectedBookmark.nickname || '请输入昵称'}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
{connected ? (
|
||||
<button className="disconnect-btn" onClick={handleDisconnect}>
|
||||
断开连接
|
||||
</button>
|
||||
) : (
|
||||
<button className="connect-btn" onClick={handleConnect}>
|
||||
连接
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder={selectedBookmark.nickname || '请输入昵称'}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
{connected ? (
|
||||
<button className="disconnect-btn" onClick={handleDisconnect}>
|
||||
断开连接
|
||||
</button>
|
||||
) : (
|
||||
<button className="connect-btn" onClick={handleConnect}>
|
||||
连接
|
||||
</button>
|
||||
|
||||
<section className="query-panel">
|
||||
<div className="query-header">
|
||||
<div>
|
||||
<h2>ServerQuery 快照</h2>
|
||||
<p>读取公开 ServerQuery 信息,默认端口通常是 10011。</p>
|
||||
</div>
|
||||
<div className="query-actions">
|
||||
<input
|
||||
type="number"
|
||||
value={queryPort}
|
||||
min={1}
|
||||
max={65535}
|
||||
onChange={(e) => setQueryPort(Number(e.target.value))}
|
||||
aria-label="ServerQuery port"
|
||||
/>
|
||||
<button className="connect-btn" onClick={handleLoadServerQuery} disabled={queryLoading}>
|
||||
{queryLoading ? '读取中...' : '读取快照'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{queryError && <div className="query-error">{queryError}</div>}
|
||||
|
||||
{querySnapshot && (
|
||||
<div className="query-grid">
|
||||
<div className="query-card">
|
||||
<h3>{querySnapshot.server?.name || '服务器'}</h3>
|
||||
<p>{querySnapshot.server?.platform || '未知平台'}</p>
|
||||
<p>{querySnapshot.server?.version || '未知版本'}</p>
|
||||
<strong>
|
||||
{querySnapshot.server?.clients_online ?? querySnapshot.clients.length}/
|
||||
{querySnapshot.server?.max_clients ?? '-'} 在线
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>频道</h3>
|
||||
<ul className="query-list">
|
||||
{querySnapshot.channels.map((channel) => (
|
||||
<li key={channel.id}>
|
||||
<span>{channel.name}</span>
|
||||
<small>{channel.total_clients} 人</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>客户端</h3>
|
||||
<ul className="query-list">
|
||||
{querySnapshot.clients.map((client) => (
|
||||
<li key={client.id}>
|
||||
<span>{client.nickname}</span>
|
||||
<small>#{client.id}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="welcome">
|
||||
|
||||
@@ -92,6 +92,12 @@ body {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.identity-summary {
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -134,6 +140,13 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.server-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 400px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.connect-form {
|
||||
max-width: 400px;
|
||||
}
|
||||
@@ -201,6 +214,11 @@ body {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.connect-btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
@@ -229,3 +247,105 @@ body {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.query-panel {
|
||||
padding: 20px;
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-header h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.query-header p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.query-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.query-actions input {
|
||||
width: 96px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.query-error {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--error-color);
|
||||
background-color: #ffebee;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.query-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-card {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
background-color: var(--background-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.query-card h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.query-card p,
|
||||
.query-card small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.query-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-list span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.server-panel,
|
||||
.query-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ tsdb = { path = "../../tsdb" }
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = "2"
|
||||
|
||||
[lib]
|
||||
name = "re_teamspeak_lib"
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use tauri_build::{build_mobile, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
build_mobile()
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
@@ -1,7 +1,12 @@
|
||||
//! Tauri 命令
|
||||
|
||||
use tauri::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared::{PermissionInfo, ServerQueryChannel, ServerQueryClient, ServerQueryServerInfo};
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use tauri::State;
|
||||
use tokio::net::lookup_host;
|
||||
use tscore::{ClientConfig, IdentityKey, QueryClient, Session};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
@@ -33,21 +38,49 @@ pub struct MessageInfo {
|
||||
pub is_read: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_identities(state: State<'_, AppState>) -> Result<Vec<IdentityInfo>, String> {
|
||||
let identities = state.db.get_all_identities().map_err(|e| e.to_string())?;
|
||||
Ok(identities.into_iter().map(|i| IdentityInfo {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
counter: i.counter,
|
||||
max_counter: i.max_counter,
|
||||
}).collect())
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ServerQuerySnapshotRequest {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub virtual_server_id: Option<u64>,
|
||||
pub include_permissions: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ServerQuerySnapshot {
|
||||
pub server: Option<ServerQueryServerInfo>,
|
||||
pub channels: Vec<ServerQueryChannel>,
|
||||
pub clients: Vec<ServerQueryClient>,
|
||||
pub permissions: Vec<PermissionInfo>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_identity(state: State<'_, AppState>, name: String) -> Result<IdentityInfo, String> {
|
||||
let private_key = "placeholder";
|
||||
let identity = state.db.create_identity(&name, private_key).map_err(|e| e.to_string())?;
|
||||
pub async fn get_identities(state: State<'_, AppState>) -> Result<Vec<IdentityInfo>, String> {
|
||||
let db = state.db.lock().await;
|
||||
let identities = db.get_all_identities().map_err(|e| e.to_string())?;
|
||||
Ok(identities
|
||||
.into_iter()
|
||||
.map(|i| IdentityInfo {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
counter: i.counter,
|
||||
max_counter: i.max_counter,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_identity(
|
||||
state: State<'_, AppState>,
|
||||
name: String,
|
||||
) -> Result<IdentityInfo, String> {
|
||||
let private_key = IdentityKey::generate().private_key_base64();
|
||||
let db = state.db.lock().await;
|
||||
let identity = db
|
||||
.create_identity(&name, &private_key)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(IdentityInfo {
|
||||
id: identity.id,
|
||||
name: identity.name,
|
||||
@@ -58,22 +91,27 @@ pub async fn create_identity(state: State<'_, AppState>, name: String) -> Result
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_identity(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.db.delete_identity(&id).map_err(|e| e.to_string())?;
|
||||
let db = state.db.lock().await;
|
||||
db.delete_identity(&id).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_bookmarks(state: State<'_, AppState>) -> Result<Vec<BookmarkInfo>, String> {
|
||||
let bookmarks = state.db.get_all_bookmarks().map_err(|e| e.to_string())?;
|
||||
Ok(bookmarks.into_iter().map(|b| BookmarkInfo {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
address: b.address,
|
||||
port: b.port,
|
||||
nickname: b.nickname,
|
||||
auto_connect: b.auto_connect,
|
||||
last_connected: b.last_connected,
|
||||
}).collect())
|
||||
let db = state.db.lock().await;
|
||||
let bookmarks = db.get_all_bookmarks().map_err(|e| e.to_string())?;
|
||||
Ok(bookmarks
|
||||
.into_iter()
|
||||
.map(|b| BookmarkInfo {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
address: b.address,
|
||||
port: b.port,
|
||||
nickname: b.nickname,
|
||||
auto_connect: b.auto_connect,
|
||||
last_connected: b.last_connected,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -84,7 +122,9 @@ pub async fn create_bookmark(
|
||||
port: u16,
|
||||
nickname: Option<String>,
|
||||
) -> Result<BookmarkInfo, String> {
|
||||
let bookmark = state.db.create_bookmark(&name, &address, port, nickname.as_deref())
|
||||
let db = state.db.lock().await;
|
||||
let bookmark = db
|
||||
.create_bookmark(&name, &address, port, nickname.as_deref())
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(BookmarkInfo {
|
||||
id: bookmark.id,
|
||||
@@ -99,7 +139,8 @@ pub async fn create_bookmark(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_bookmark(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.db.delete_bookmark(&id).map_err(|e| e.to_string())?;
|
||||
let db = state.db.lock().await;
|
||||
db.delete_bookmark(&id).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -111,29 +152,125 @@ pub async fn connect(
|
||||
nickname: String,
|
||||
password: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let socket_addr = resolve_server_address(&address, port).await?;
|
||||
let identity = {
|
||||
let db = state.db.lock().await;
|
||||
db.get_all_identities()
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|identity| IdentityKey::from_private_key_base64(&identity.private_key).ok())
|
||||
.unwrap_or_else(IdentityKey::generate)
|
||||
};
|
||||
|
||||
let mut config = ClientConfig::new(socket_addr, nickname.clone());
|
||||
config.server_password = password;
|
||||
config.identity = identity;
|
||||
|
||||
let (mut session, handle) = Session::connect(config, Duration::from_secs(15))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let client_id = session.client_id();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = session.run().await {
|
||||
tracing::error!("session error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let mut session_guard = state.session_handle.lock().await;
|
||||
*session_guard = Some(handle);
|
||||
}
|
||||
|
||||
let mut conn_state = state.connection_state.lock().await;
|
||||
conn_state.connected = true;
|
||||
conn_state.server_address = Some(address.clone());
|
||||
conn_state.server_address = Some(address);
|
||||
conn_state.server_port = Some(port);
|
||||
conn_state.nickname = Some(nickname);
|
||||
conn_state.client_id = client_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let handle = {
|
||||
let mut session_guard = state.session_handle.lock().await;
|
||||
session_guard.take()
|
||||
};
|
||||
|
||||
if let Some(handle) = handle {
|
||||
handle.disconnect().await.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut conn_state = state.connection_state.lock().await;
|
||||
*conn_state = crate::state::ConnectionState::new();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_message(
|
||||
pub async fn join_channel(
|
||||
state: State<'_, AppState>,
|
||||
channel_id: u64,
|
||||
password: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.join_channel(channel_id, password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_channel_message(
|
||||
state: State<'_, AppState>,
|
||||
target: String,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
// TODO: 实现发送消息
|
||||
Ok(())
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.send_channel_message(&message)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_server_message(
|
||||
state: State<'_, AppState>,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.send_server_message(&message)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_private_message(
|
||||
state: State<'_, AppState>,
|
||||
client_id: u64,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.send_private_message(client_id, &message)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_raw_command(state: State<'_, AppState>, command: String) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.send_command_str(&command)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -143,13 +280,73 @@ pub async fn get_messages(
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
let messages = state.db.get_server_messages(&server_address, limit, offset)
|
||||
let db = state.db.lock().await;
|
||||
let messages = db
|
||||
.get_server_messages(&server_address, limit, offset)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(messages.into_iter().map(|m| MessageInfo {
|
||||
id: m.id,
|
||||
invoker_name: m.invoker_name,
|
||||
message: m.message,
|
||||
timestamp: m.timestamp,
|
||||
is_read: m.is_read,
|
||||
}).collect())
|
||||
Ok(messages
|
||||
.into_iter()
|
||||
.map(|m| MessageInfo {
|
||||
id: m.id,
|
||||
invoker_name: m.invoker_name,
|
||||
message: m.message,
|
||||
timestamp: m.timestamp,
|
||||
is_read: m.is_read,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn server_query_snapshot(
|
||||
request: ServerQuerySnapshotRequest,
|
||||
) -> Result<ServerQuerySnapshot, String> {
|
||||
let socket_addr = resolve_server_address(&request.address, request.port).await?;
|
||||
let mut client = QueryClient::connect(socket_addr)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
client.set_read_timeout(Duration::from_secs(5));
|
||||
|
||||
if let (Some(username), Some(password)) =
|
||||
(request.username.as_deref(), request.password.as_deref())
|
||||
{
|
||||
client
|
||||
.login(username, password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
if let Some(server_id) = request.virtual_server_id {
|
||||
client
|
||||
.use_server(server_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let server = client.server_info().await.map_err(|e| e.to_string())?;
|
||||
let channels = client.channel_list().await.map_err(|e| e.to_string())?;
|
||||
let clients = client.client_list().await.map_err(|e| e.to_string())?;
|
||||
let permissions = if request.include_permissions {
|
||||
client.permission_list().await.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(ServerQuerySnapshot {
|
||||
server,
|
||||
channels,
|
||||
clients,
|
||||
permissions,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_server_address(address: &str, port: u16) -> Result<SocketAddr, String> {
|
||||
if let Ok(socket_addr) = format!("{}:{}", address, port).parse::<SocketAddr>() {
|
||||
return Ok(socket_addr);
|
||||
}
|
||||
|
||||
lookup_host((address, port))
|
||||
.await
|
||||
.map_err(|e| format!("无法解析服务器地址: {e}"))?
|
||||
.next()
|
||||
.ok_or_else(|| "无法解析服务器地址".to_string())
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ mod commands;
|
||||
mod state;
|
||||
|
||||
pub struct AppState {
|
||||
pub db: tsdb::DatabaseManager,
|
||||
pub db: tokio::sync::Mutex<tsdb::DatabaseManager>,
|
||||
pub connection_state: tokio::sync::Mutex<state::ConnectionState>,
|
||||
pub session_handle: tokio::sync::Mutex<Option<tscore::SessionHandle>>,
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
@@ -24,12 +25,13 @@ pub fn run() {
|
||||
std::fs::create_dir_all(&app_dir).expect("无法创建应用数据目录");
|
||||
|
||||
let db_path = app_dir.join("re-teamspeak.db");
|
||||
let db = tsdb::DatabaseManager::new(db_path.to_str().unwrap())
|
||||
.expect("无法初始化数据库");
|
||||
let db =
|
||||
tsdb::DatabaseManager::new(db_path.to_str().unwrap()).expect("无法初始化数据库");
|
||||
|
||||
let state = AppState {
|
||||
db,
|
||||
db: tokio::sync::Mutex::new(db),
|
||||
connection_state: tokio::sync::Mutex::new(state::ConnectionState::new()),
|
||||
session_handle: tokio::sync::Mutex::new(None),
|
||||
};
|
||||
app.manage(state);
|
||||
|
||||
@@ -44,8 +46,13 @@ pub fn run() {
|
||||
commands::delete_bookmark,
|
||||
commands::connect,
|
||||
commands::disconnect,
|
||||
commands::send_message,
|
||||
commands::join_channel,
|
||||
commands::send_channel_message,
|
||||
commands::send_server_message,
|
||||
commands::send_private_message,
|
||||
commands::send_raw_command,
|
||||
commands::get_messages,
|
||||
commands::server_query_snapshot,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("运行应用时出错");
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"beforeBuildCommand": "cd ../frontend && npm run build"
|
||||
},
|
||||
"app": {
|
||||
"title": "ReTeamSpeak",
|
||||
"windows": [
|
||||
{
|
||||
"title": "ReTeamSpeak",
|
||||
@@ -28,14 +27,7 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
"active": false,
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user