diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b666624..4816906 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ on: env: CARGO_TERM_COLOR: always - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: # Test job diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index 076b54a..0000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: opencode -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - -jobs: - opencode: - if: | - contains(github.event.comment.body, '/oc') || - contains(github.event.comment.body, '/opencode') - runs-on: ubuntu-latest - permissions: - id-token: write - contents: write - pull-requests: write - issues: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run OpenCode - uses: anomalyco/opencode/github@latest - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - with: - model: anthropic/claude-sonnet-4-20250514 diff --git a/src/tscore/src/connection/session.rs b/src/tscore/src/connection/session.rs index 9cc6604..3764f09 100644 --- a/src/tscore/src/connection/session.rs +++ b/src/tscore/src/connection/session.rs @@ -4,6 +4,7 @@ use tokio::sync::mpsc; use super::client::{Client, ClientConfig}; use super::state::ConnectionState; +use crate::protocol::{Command, Direction, InPacket, PacketType}; use crate::ProtocolError; pub enum SessionCommand { @@ -21,6 +22,8 @@ pub enum SessionCommand { target_id: u64, message: String, }, + RequestChannelList, + RequestClientList, Disconnect, } @@ -30,8 +33,66 @@ pub enum TextMessageTarget { Client = 1, } +#[derive(Debug, Clone, serde::Serialize)] +pub enum SessionEvent { + Connected { + client_id: u16, + }, + ChannelList(Vec), + ClientList(Vec), + ClientEntered { + clid: u16, + cid: u64, + client_nickname: String, + }, + ClientLeft { + clid: u16, + reason: String, + }, + ClientMoved { + clid: u16, + cid: u64, + }, + TextMessage { + invoker_id: u16, + invoker_name: String, + message: String, + target_mode: u8, + }, + ServerInfo { + name: String, + platform: String, + version: String, + max_clients: u16, + clients_online: u16, + channels_online: u16, + }, + Error(String), + Disconnected, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ChannelEntry { + pub cid: u64, + pub pid: u64, + pub channel_order: u64, + pub channel_name: String, + pub total_clients: u16, + pub channel_needed_subscribe_power: i32, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ClientEntry { + pub clid: u16, + pub cid: u64, + pub client_database_id: u64, + pub client_nickname: String, + pub client_type: u8, +} + pub struct SessionHandle { command_tx: mpsc::Sender, + event_rx: mpsc::Receiver, } impl SessionHandle { @@ -107,12 +168,34 @@ impl SessionHandle { .map_err(|_| ProtocolError::ConnectionClosed) } + pub async fn request_channel_list(&self) -> Result<(), ProtocolError> { + self.command_tx + .send(SessionCommand::RequestChannelList) + .await + .map_err(|_| ProtocolError::ConnectionClosed) + } + + pub async fn request_client_list(&self) -> Result<(), ProtocolError> { + self.command_tx + .send(SessionCommand::RequestClientList) + .await + .map_err(|_| ProtocolError::ConnectionClosed) + } + pub async fn disconnect(&self) -> Result<(), ProtocolError> { self.command_tx .send(SessionCommand::Disconnect) .await .map_err(|_| ProtocolError::ConnectionClosed) } + + pub async fn recv_event(&mut self) -> Option { + self.event_rx.recv().await + } + + pub fn try_recv_event(&mut self) -> Option { + self.event_rx.try_recv().ok() + } } pub struct Session { @@ -122,38 +205,6 @@ pub struct Session { event_tx: mpsc::Sender, } -pub enum SessionEvent { - Connected { - client_id: u16, - }, - ChannelList(Vec), - ClientEntered { - clid: u16, - cid: u64, - client_nickname: String, - }, - ClientLeft { - clid: u16, - }, - TextMessage { - invoker_id: u16, - invoker_name: String, - message: String, - }, - Error(String), - Disconnected, -} - -#[derive(Debug, Clone)] -pub struct ChannelEntry { - pub cid: u64, - pub pid: u64, - pub channel_order: u64, - pub channel_name: String, - pub total_clients: u16, - pub channel_needed_subscribe_power: i32, -} - impl Session { pub async fn connect( config: ClientConfig, @@ -171,14 +222,27 @@ impl Session { let init0 = client.start_handshake()?; socket.send(&init0).await?; + let mut init_events = Vec::new(); + tokio::time::timeout(timeout, async { let mut buf = [0u8; 2048]; loop { let len = socket.recv(&mut buf).await?; let responses = client.handle_data(&buf[..len])?; - for response in responses { - socket.send(&response).await?; + for response in &responses { + socket.send(response).await?; } + + if let Ok(packet) = InPacket::parse(Direction::S2C, &buf[..len]) { + if let Some(content) = Self::decrypt_packet_static(&client, &packet) { + if let Ok(content_str) = std::str::from_utf8(&content) { + for cmd in Command::parse_many(content_str).unwrap_or_default() { + Self::collect_init_event(&cmd, &mut init_events); + } + } + } + } + if client.state() == ConnectionState::ChannelListFinished { return Ok::<(), ProtocolError>(()); } @@ -188,7 +252,7 @@ impl Session { .map_err(|_| ProtocolError::Timeout("handshake timed out".to_string()))??; let (command_tx, command_rx) = mpsc::channel(32); - let (event_tx, _event_rx) = mpsc::channel(32); + let (event_tx, mut event_rx) = mpsc::channel(32); let session = Self { client, @@ -197,11 +261,74 @@ impl Session { event_tx, }; - let handle = SessionHandle { command_tx }; + let handle = SessionHandle { + command_tx, + event_rx, + }; Ok((session, handle)) } + fn decrypt_packet_static(client: &Client, packet: &InPacket) -> Option> { + let packet_type = packet.header.flags.packet_type(); + if packet.header.flags.is_unencrypted() { + return Some(packet.data.clone()); + } + if packet_type == PacketType::Ack || packet_type == PacketType::AckLow { + return None; + } + None + } + + fn collect_init_event(cmd: &Command, events: &mut Vec) { + 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); + + events.push(SessionEvent::ServerInfo { + name, + platform, + version, + max_clients, + clients_online, + channels_online, + }); + events.push(SessionEvent::Connected { client_id }); + } + "channellist" => { + let channels = Self::parse_channel_list(cmd); + events.push(SessionEvent::ChannelList(channels)); + } + _ => {} + } + } + + fn parse_channel_list(cmd: &Command) -> Vec { + 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_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); + + channels.push(ChannelEntry { + cid, + pid, + channel_order, + channel_name, + total_clients, + channel_needed_subscribe_power, + }); + channels + } + pub fn client_id(&self) -> Option { self.client.client_id() } @@ -218,8 +345,21 @@ impl Session { result = self.socket.recv(&mut buf) => { let len = result?; let responses = self.client.handle_data(&buf[..len])?; - for response in responses { - self.socket.send(&response).await?; + for response in &responses { + self.socket.send(response).await?; + } + + if let Ok(packet) = InPacket::parse(Direction::S2C, &buf[..len]) { + let packet_type = packet.header.flags.packet_type(); + if packet_type == PacketType::Command || packet_type == PacketType::CommandLow { + if let Some(content) = self.decrypt_packet(&packet) { + if let Ok(content_str) = std::str::from_utf8(&content) { + for cmd in Command::parse_many(content_str).unwrap_or_default() { + self.emit_command_event(&cmd).await; + } + } + } + } } } Some(command) = self.command_rx.recv() => { @@ -238,6 +378,111 @@ impl Session { } } + fn decrypt_packet(&self, packet: &InPacket) -> Option> { + let packet_type = packet.header.flags.packet_type(); + if packet.header.flags.is_unencrypted() { + return Some(packet.data.clone()); + } + if packet_type == PacketType::Ack || packet_type == PacketType::AckLow { + return None; + } + None + } + + 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 _ = self.event_tx.send(SessionEvent::ServerInfo { + name, + platform, + version, + max_clients, + clients_online, + channels_online, + }).await; + Some(SessionEvent::Connected { client_id }) + } + "channellist" => { + let channels = Self::parse_channel_list(cmd); + Some(SessionEvent::ChannelList(channels)) + } + "clientlist" => { + let clients = Self::parse_client_list(cmd); + Some(SessionEvent::ClientList(clients)) + } + "notifycliententerview" => { + 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_nickname = cmd.get("client_nickname").unwrap_or_default().to_string(); + Some(SessionEvent::ClientEntered { + clid, + cid, + client_nickname, + }) + } + "notifyclientleftview" => { + let clid = cmd.get("clid").and_then(|v| v.parse().ok()).unwrap_or(0); + let reason = cmd.get("reasonmsg").unwrap_or_default().to_string(); + Some(SessionEvent::ClientLeft { clid, reason }) + } + "notifyclientmoved" => { + let clid = cmd.get("clid").and_then(|v| v.parse().ok()).unwrap_or(0); + let cid = cmd.get("ctid").and_then(|v| v.parse().ok()).unwrap_or(0); + Some(SessionEvent::ClientMoved { clid, cid }) + } + "notifytextmessage" => { + 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); + Some(SessionEvent::TextMessage { + invoker_id, + invoker_name, + message, + target_mode, + }) + } + "error" => { + let id = cmd.get("id").unwrap_or("0"); + if id != "0" { + let msg = cmd.get("msg").unwrap_or("unknown error").to_string(); + Some(SessionEvent::Error(msg)) + } else { + None + } + } + _ => None, + }; + + if let Some(event) = event { + let _ = self.event_tx.send(event).await; + } + } + + fn parse_client_list(cmd: &Command) -> Vec { + 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_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); + + vec![ClientEntry { + clid, + cid, + client_database_id, + client_nickname, + client_type, + }] + } + async fn handle_command(&mut self, command: SessionCommand) -> Result<(), ProtocolError> { match command { SessionCommand::SendCommand(content) => { @@ -278,6 +523,14 @@ impl Session { let packet = self.client.build_command_packet(cmd.into_bytes())?; self.socket.send(&packet).await?; } + SessionCommand::RequestChannelList => { + let packet = self.client.build_command_packet(b"channellist".to_vec())?; + self.socket.send(&packet).await?; + } + SessionCommand::RequestClientList => { + let packet = self.client.build_command_packet(b"clientlist".to_vec())?; + self.socket.send(&packet).await?; + } SessionCommand::Disconnect => { let packet = self .client