ci: revert FORCE_JAVASCRIPT_ACTIONS_TO_NODE24, let v4 actions run on Node.js 20

This commit is contained in:
ReTeamSpeak
2026-05-12 18:58:34 +09:00
parent 364b24bf19
commit a1410b541b
3 changed files with 291 additions and 73 deletions
-1
View File
@@ -10,7 +10,6 @@ on:
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
# Test job
-34
View File
@@ -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
+291 -38
View File
@@ -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<ChannelEntry>),
ClientList(Vec<ClientEntry>),
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<SessionCommand>,
event_rx: mpsc::Receiver<SessionEvent>,
}
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<SessionEvent> {
self.event_rx.recv().await
}
pub fn try_recv_event(&mut self) -> Option<SessionEvent> {
self.event_rx.try_recv().ok()
}
}
pub struct Session {
@@ -122,38 +205,6 @@ pub struct Session {
event_tx: mpsc::Sender<SessionEvent>,
}
pub enum SessionEvent {
Connected {
client_id: u16,
},
ChannelList(Vec<ChannelEntry>),
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<Vec<u8>> {
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<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);
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<ChannelEntry> {
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<u16> {
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<Vec<u8>> {
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<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_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