feat: add session event polling, channel/client list, and text messages
- Add SessionEvent enum with Connected, ChannelList, ClientList, ServerInfo, TextMessage, ClientEntered, ClientLeft, ClientMoved - Add poll_events, request_channel_list, request_client_list Tauri commands - Session now parses initserver, channellist, clientlist, notify* commands and emits events - Frontend polls events every 500ms when connected - Frontend displays server info, channel list, client list, and text messages
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface Identity {
|
||||
@@ -18,6 +18,51 @@ interface Bookmark {
|
||||
last_connected: string | null;
|
||||
}
|
||||
|
||||
interface ChannelEntry {
|
||||
cid: number;
|
||||
pid: number;
|
||||
channel_order: number;
|
||||
channel_name: string;
|
||||
total_clients: number;
|
||||
channel_needed_subscribe_power: number;
|
||||
}
|
||||
|
||||
interface ClientEntry {
|
||||
clid: number;
|
||||
cid: number;
|
||||
client_database_id: number;
|
||||
client_nickname: string;
|
||||
client_type: number;
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
name: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
max_clients: number;
|
||||
clients_online: number;
|
||||
channels_online: number;
|
||||
}
|
||||
|
||||
interface TextMessage {
|
||||
invoker_id: number;
|
||||
invoker_name: string;
|
||||
message: string;
|
||||
target_mode: number;
|
||||
}
|
||||
|
||||
type SessionEvent =
|
||||
| { Connected: { client_id: number } }
|
||||
| { ChannelList: ChannelEntry[] }
|
||||
| { ClientList: ClientEntry[] }
|
||||
| { ServerInfo: ServerInfo }
|
||||
| { TextMessage: TextMessage }
|
||||
| { ClientEntered: { clid: number; cid: number; client_nickname: string } }
|
||||
| { ClientLeft: { clid: number; reason: string } }
|
||||
| { ClientMoved: { clid: number; cid: number } }
|
||||
| { Error: string }
|
||||
| { Disconnected: null };
|
||||
|
||||
interface ServerQueryChannel {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -52,10 +97,15 @@ function App() {
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
||||
const [channels, setChannels] = useState<ChannelEntry[]>([]);
|
||||
const [clients, setClients] = useState<ClientEntry[]>([]);
|
||||
const [messages, setMessages] = useState<TextMessage[]>([]);
|
||||
const [queryPort, setQueryPort] = useState(10011);
|
||||
const [querySnapshot, setQuerySnapshot] = useState<ServerQuerySnapshot | null>(null);
|
||||
const [queryLoading, setQueryLoading] = useState(false);
|
||||
const [queryError, setQueryError] = useState<string | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadIdentities();
|
||||
@@ -67,6 +117,56 @@ function App() {
|
||||
setQueryError(null);
|
||||
}, [selectedBookmark]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
pollEvents();
|
||||
pollRef.current = setInterval(pollEvents, 500);
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
};
|
||||
} else {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
async function pollEvents() {
|
||||
try {
|
||||
const events = await invoke<SessionEvent[]>('poll_events');
|
||||
for (const event of events) {
|
||||
handleEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to poll events:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: SessionEvent) {
|
||||
if ('Connected' in event) {
|
||||
console.log('Connected as client', event.Connected.client_id);
|
||||
} else if ('ServerInfo' in event) {
|
||||
setServerInfo(event.ServerInfo);
|
||||
} else if ('ChannelList' in event) {
|
||||
setChannels(event.ChannelList);
|
||||
} else if ('ClientList' in event) {
|
||||
setClients(event.ClientList);
|
||||
} else if ('TextMessage' in event) {
|
||||
setMessages((prev) => [...prev, event.TextMessage]);
|
||||
} else if ('ClientEntered' in event) {
|
||||
console.log('Client entered:', event.ClientEntered);
|
||||
} else if ('ClientLeft' in event) {
|
||||
console.log('Client left:', event.ClientLeft);
|
||||
} else if ('ClientMoved' in event) {
|
||||
console.log('Client moved:', event.ClientMoved);
|
||||
} else if ('Error' in event) {
|
||||
console.error('Session error:', event.Error);
|
||||
} else if ('Disconnected' in event) {
|
||||
setConnected(false);
|
||||
setServerInfo(null);
|
||||
setChannels([]);
|
||||
setClients([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
const result = await invoke<Identity[]>('get_identities');
|
||||
@@ -96,6 +196,7 @@ function App() {
|
||||
password: password || null,
|
||||
});
|
||||
setConnected(true);
|
||||
setMessages([]);
|
||||
} catch (error) {
|
||||
console.error('Failed to connect:', error);
|
||||
}
|
||||
@@ -105,6 +206,9 @@ function App() {
|
||||
try {
|
||||
await invoke('disconnect');
|
||||
setConnected(false);
|
||||
setServerInfo(null);
|
||||
setChannels([]);
|
||||
setClients([]);
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect:', error);
|
||||
}
|
||||
@@ -141,7 +245,9 @@ function App() {
|
||||
<h1>ReTeamSpeak</h1>
|
||||
<div className="connection-status">
|
||||
{connected ? (
|
||||
<span className="status connected">已连接</span>
|
||||
<span className="status connected">
|
||||
已连接 {serverInfo ? `— ${serverInfo.name}` : ''}
|
||||
</span>
|
||||
) : (
|
||||
<span className="status disconnected">未连接</span>
|
||||
)}
|
||||
@@ -212,6 +318,60 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connected && (
|
||||
<section className="session-panel">
|
||||
<div className="query-grid">
|
||||
<div className="query-card">
|
||||
<h3>服务器</h3>
|
||||
{serverInfo ? (
|
||||
<>
|
||||
<p><strong>{serverInfo.name}</strong></p>
|
||||
<p>{serverInfo.platform} / {serverInfo.version}</p>
|
||||
<p>{serverInfo.clients_online} / {serverInfo.max_clients} 在线</p>
|
||||
</>
|
||||
) : (
|
||||
<p>等待服务器信息...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>频道 ({channels.length})</h3>
|
||||
<ul className="query-list">
|
||||
{channels.map((ch) => (
|
||||
<li key={ch.cid}>
|
||||
<span>{ch.channel_name}</span>
|
||||
<small>{ch.total_clients} 人</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>客户端 ({clients.length})</h3>
|
||||
<ul className="query-list">
|
||||
{clients.map((c) => (
|
||||
<li key={c.clid}>
|
||||
<span>{c.client_nickname}</span>
|
||||
<small>#{c.clid}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="messages-panel">
|
||||
<h3>消息 ({messages.length})</h3>
|
||||
<ul className="message-list">
|
||||
{messages.map((msg, i) => (
|
||||
<li key={i}>
|
||||
<strong>{msg.invoker_name}:</strong> {msg.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="query-panel">
|
||||
<div className="query-header">
|
||||
<div>
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use tauri::State;
|
||||
use tokio::net::lookup_host;
|
||||
use tscore::{ClientConfig, IdentityKey, QueryClient, Session};
|
||||
use tscore::{ClientConfig, IdentityKey, QueryClient, Session, SessionEvent};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
@@ -273,6 +273,37 @@ pub async fn send_raw_command(state: State<'_, AppState>, command: String) -> Re
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn poll_events(state: State<'_, AppState>) -> Result<Vec<SessionEvent>, String> {
|
||||
let mut session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_mut().ok_or("not connected")?;
|
||||
let mut events = Vec::new();
|
||||
while let Some(event) = handle.try_recv_event() {
|
||||
events.push(event);
|
||||
}
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn request_channel_list(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.request_channel_list()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn request_client_list(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let session_guard = state.session_handle.lock().await;
|
||||
let handle = session_guard.as_ref().ok_or("not connected")?;
|
||||
handle
|
||||
.request_client_list()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_messages(
|
||||
state: State<'_, AppState>,
|
||||
|
||||
@@ -51,6 +51,9 @@ pub fn run() {
|
||||
commands::send_server_message,
|
||||
commands::send_private_message,
|
||||
commands::send_raw_command,
|
||||
commands::poll_events,
|
||||
commands::request_channel_list,
|
||||
commands::request_client_list,
|
||||
commands::get_messages,
|
||||
commands::server_query_snapshot,
|
||||
])
|
||||
|
||||
@@ -283,13 +283,34 @@ impl Session {
|
||||
fn collect_init_event(cmd: &Command, events: &mut Vec<SessionEvent>) {
|
||||
match cmd.name.as_str() {
|
||||
"initserver" => {
|
||||
let client_id = cmd.get("client_id").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let name = cmd.get("virtualserver_name").unwrap_or_default().to_string();
|
||||
let platform = cmd.get("virtualserver_platform").unwrap_or_default().to_string();
|
||||
let version = cmd.get("virtualserver_version").unwrap_or_default().to_string();
|
||||
let max_clients = cmd.get("virtualserver_maxclients").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let clients_online = cmd.get("virtualserver_clientsonline").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let channels_online = cmd.get("virtualserver_channelsonline").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client_id = cmd
|
||||
.get("client_id")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let name = cmd
|
||||
.get("virtualserver_name")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let platform = cmd
|
||||
.get("virtualserver_platform")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let version = cmd
|
||||
.get("virtualserver_version")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let max_clients = cmd
|
||||
.get("virtualserver_maxclients")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let clients_online = cmd
|
||||
.get("virtualserver_clientsonline")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let channels_online = cmd
|
||||
.get("virtualserver_channelsonline")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
events.push(SessionEvent::ServerInfo {
|
||||
name,
|
||||
@@ -313,10 +334,19 @@ impl Session {
|
||||
let mut channels = Vec::new();
|
||||
let cid = cmd.get("cid").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let pid = cmd.get("pid").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let channel_order = cmd.get("channel_order").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let channel_order = cmd
|
||||
.get("channel_order")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let channel_name = cmd.get("channel_name").unwrap_or_default().to_string();
|
||||
let total_clients = cmd.get("total_clients").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let channel_needed_subscribe_power = cmd.get("channel_needed_subscribe_power").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let total_clients = cmd
|
||||
.get("total_clients")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let channel_needed_subscribe_power = cmd
|
||||
.get("channel_needed_subscribe_power")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
channels.push(ChannelEntry {
|
||||
cid,
|
||||
@@ -392,22 +422,46 @@ impl Session {
|
||||
async fn emit_command_event(&self, cmd: &Command) {
|
||||
let event = match cmd.name.as_str() {
|
||||
"initserver" => {
|
||||
let client_id = cmd.get("client_id").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let name = cmd.get("virtualserver_name").unwrap_or_default().to_string();
|
||||
let platform = cmd.get("virtualserver_platform").unwrap_or_default().to_string();
|
||||
let version = cmd.get("virtualserver_version").unwrap_or_default().to_string();
|
||||
let max_clients = cmd.get("virtualserver_maxclients").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let clients_online = cmd.get("virtualserver_clientsonline").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let channels_online = cmd.get("virtualserver_channelsonline").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client_id = cmd
|
||||
.get("client_id")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let name = cmd
|
||||
.get("virtualserver_name")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let platform = cmd
|
||||
.get("virtualserver_platform")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let version = cmd
|
||||
.get("virtualserver_version")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let max_clients = cmd
|
||||
.get("virtualserver_maxclients")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let clients_online = cmd
|
||||
.get("virtualserver_clientsonline")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let channels_online = cmd
|
||||
.get("virtualserver_channelsonline")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let _ = self.event_tx.send(SessionEvent::ServerInfo {
|
||||
name,
|
||||
platform,
|
||||
version,
|
||||
max_clients,
|
||||
clients_online,
|
||||
channels_online,
|
||||
}).await;
|
||||
let _ = self
|
||||
.event_tx
|
||||
.send(SessionEvent::ServerInfo {
|
||||
name,
|
||||
platform,
|
||||
version,
|
||||
max_clients,
|
||||
clients_online,
|
||||
channels_online,
|
||||
})
|
||||
.await;
|
||||
Some(SessionEvent::Connected { client_id })
|
||||
}
|
||||
"channellist" => {
|
||||
@@ -439,10 +493,16 @@ impl Session {
|
||||
Some(SessionEvent::ClientMoved { clid, cid })
|
||||
}
|
||||
"notifytextmessage" => {
|
||||
let invoker_id = cmd.get("invokerid").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let invoker_id = cmd
|
||||
.get("invokerid")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let invoker_name = cmd.get("invokername").unwrap_or_default().to_string();
|
||||
let message = cmd.get("msg").unwrap_or_default().to_string();
|
||||
let target_mode = cmd.get("targetmode").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let target_mode = cmd
|
||||
.get("targetmode")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
Some(SessionEvent::TextMessage {
|
||||
invoker_id,
|
||||
invoker_name,
|
||||
@@ -470,9 +530,15 @@ impl Session {
|
||||
fn parse_client_list(cmd: &Command) -> Vec<ClientEntry> {
|
||||
let clid = cmd.get("clid").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let cid = cmd.get("cid").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client_database_id = cmd.get("client_database_id").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client_database_id = cmd
|
||||
.get("client_database_id")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let client_nickname = cmd.get("client_nickname").unwrap_or_default().to_string();
|
||||
let client_type = cmd.get("client_type").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client_type = cmd
|
||||
.get("client_type")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
vec![ClientEntry {
|
||||
clid,
|
||||
|
||||
@@ -110,13 +110,17 @@ impl CommandArgument {
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandArgument {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.value {
|
||||
Some(value) => write!(f, "{}={}", escape::escape(&self.name), escape::escape(value)),
|
||||
Some(value) => write!(
|
||||
f,
|
||||
"{}={}",
|
||||
escape::escape(&self.name),
|
||||
escape::escape(value)
|
||||
),
|
||||
None => write!(f, "{}", escape::escape(&self.name)),
|
||||
}
|
||||
}
|
||||
@@ -226,7 +230,6 @@ impl Command {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl fmt::Display for Command {
|
||||
|
||||
Reference in New Issue
Block a user