mirror of
https://github.com/iv-org/inv_sig_helper.git
synced 2024-11-23 05:59:44 +00:00
refactor: simplify code, use format!, more idiomatic code
This commit is contained in:
parent
5025e49e61
commit
369b94e7e3
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -394,6 +394,12 @@ version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -533,6 +539,8 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rquickjs",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tub",
|
||||
@ -969,6 +977,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
@ -1090,6 +1104,25 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
|
@ -16,6 +16,8 @@ tokio-util = { version = "0.7.10", features=["futures-io", "futures-util", "code
|
||||
futures = "0.3.30"
|
||||
log = "0.4.22"
|
||||
env_logger = "0.11.5"
|
||||
strum = "0.26.3"
|
||||
strum_macros = "0.26.4"
|
||||
|
||||
# Compilation optimizations for release builds
|
||||
# Increases compile time but typically produces a faster and smaller binary. Suitable for final releases but not for debug builds.
|
||||
|
@ -25,4 +25,4 @@ pub static REGEX_SIGNATURE_FUNCTION: &Lazy<Regex> =
|
||||
pub static REGEX_HELPER_OBJ_NAME: &Lazy<Regex> = regex!(";([A-Za-z0-9_\\$]{2,})\\...\\(");
|
||||
|
||||
pub static NSIG_FUNCTION_NAME: &str = "decrypt_nsig";
|
||||
pub static SIG_FUNCTION_NAME: &str = "decrypt_sig";
|
||||
pub static _SIG_FUNCTION_NAME: &str = "decrypt_sig";
|
||||
|
81
src/jobs.rs
81
src/jobs.rs
@ -1,46 +1,27 @@
|
||||
use futures::SinkExt;
|
||||
use log::{debug, error};
|
||||
use rquickjs::{async_with, AsyncContext, AsyncRuntime};
|
||||
use std::{num::NonZeroUsize, sync::Arc, thread::available_parallelism, time::SystemTime};
|
||||
use log::{debug, error};
|
||||
use strum_macros::{Display, FromRepr};
|
||||
use tokio::{runtime::Handle, sync::Mutex, task::block_in_place};
|
||||
use tub::Pool;
|
||||
|
||||
use crate::{consts::NSIG_FUNCTION_NAME, opcode::OpcodeResponse, player::fetch_update};
|
||||
|
||||
#[derive(Display, FromRepr)]
|
||||
pub enum JobOpcode {
|
||||
ForceUpdate,
|
||||
DecryptNSignature,
|
||||
ForceUpdate = 0,
|
||||
DecryptNSignature = 1,
|
||||
DecryptSignature,
|
||||
GetSignatureTimestamp,
|
||||
PlayerStatus,
|
||||
PlayerUpdateTimestamp,
|
||||
UnknownOpcode,
|
||||
UnknownOpcode = 255,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobOpcode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ForceUpdate => write!(f, "ForceUpdate"),
|
||||
Self::DecryptNSignature => write!(f, "DecryptNSignature"),
|
||||
Self::DecryptSignature => write!(f, "DecryptSignature"),
|
||||
Self::GetSignatureTimestamp => write!(f, "GetSignatureTimestamp"),
|
||||
Self::PlayerStatus => write!(f, "PlayerStatus"),
|
||||
Self::PlayerUpdateTimestamp => write!(f, "PlayerUpdateTimestamp"),
|
||||
Self::UnknownOpcode => write!(f, "UnknownOpcode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<u8> for JobOpcode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0x00 => Self::ForceUpdate,
|
||||
0x01 => Self::DecryptNSignature,
|
||||
0x02 => Self::DecryptSignature,
|
||||
0x03 => Self::GetSignatureTimestamp,
|
||||
0x04 => Self::PlayerStatus,
|
||||
0x05 => Self::PlayerUpdateTimestamp,
|
||||
_ => Self::UnknownOpcode,
|
||||
}
|
||||
JobOpcode::from_repr(value as usize).unwrap_or(Self::UnknownOpcode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +35,22 @@ pub struct PlayerInfo {
|
||||
pub last_update: SystemTime,
|
||||
}
|
||||
|
||||
impl Default for PlayerInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nsig_function_code: Default::default(),
|
||||
sig_function_code: Default::default(),
|
||||
sig_function_name: Default::default(),
|
||||
signature_timestamp: Default::default(),
|
||||
player_id: Default::default(),
|
||||
has_player: Default::default(),
|
||||
last_update: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JavascriptInterpreter {
|
||||
#[allow(dead_code)]
|
||||
js_runtime: AsyncRuntime,
|
||||
sig_context: AsyncContext,
|
||||
nsig_context: AsyncContext,
|
||||
@ -65,17 +61,20 @@ pub struct JavascriptInterpreter {
|
||||
impl JavascriptInterpreter {
|
||||
pub fn new() -> JavascriptInterpreter {
|
||||
let js_runtime = AsyncRuntime::new().unwrap();
|
||||
|
||||
// not ideal, but this is only done at startup
|
||||
let nsig_context = block_in_place(|| {
|
||||
Handle::current()
|
||||
.block_on(AsyncContext::full(&js_runtime))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let sig_context = block_in_place(|| {
|
||||
Handle::current()
|
||||
.block_on(AsyncContext::full(&js_runtime))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
JavascriptInterpreter {
|
||||
js_runtime,
|
||||
sig_context,
|
||||
@ -98,22 +97,16 @@ impl GlobalState {
|
||||
.get();
|
||||
let mut runtime_vector: Vec<Arc<JavascriptInterpreter>> =
|
||||
Vec::with_capacity(number_of_runtimes);
|
||||
for _n in 0..number_of_runtimes {
|
||||
|
||||
for _ in 0..number_of_runtimes {
|
||||
runtime_vector.push(Arc::new(JavascriptInterpreter::new()));
|
||||
}
|
||||
|
||||
let runtime_pool: Pool<Arc<JavascriptInterpreter>> = Pool::from_vec(runtime_vector);
|
||||
let js_runtime_pool: Pool<Arc<JavascriptInterpreter>> = Pool::from_vec(runtime_vector);
|
||||
|
||||
GlobalState {
|
||||
player_info: Mutex::new(PlayerInfo {
|
||||
nsig_function_code: Default::default(),
|
||||
sig_function_code: Default::default(),
|
||||
sig_function_name: Default::default(),
|
||||
player_id: Default::default(),
|
||||
signature_timestamp: Default::default(),
|
||||
has_player: 0x00,
|
||||
last_update: SystemTime::now(),
|
||||
}),
|
||||
js_runtime_pool: runtime_pool,
|
||||
player_info: Mutex::new(PlayerInfo::default()),
|
||||
js_runtime_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,11 +176,7 @@ pub async fn process_decrypt_n_signature<W>(
|
||||
}
|
||||
drop(player_info);
|
||||
|
||||
let mut call_string: String = String::new();
|
||||
call_string += NSIG_FUNCTION_NAME;
|
||||
call_string += "(\"";
|
||||
call_string += &sig.replace("\"", "\\\"");
|
||||
call_string += "\")";
|
||||
let call_string = format!("{NSIG_FUNCTION_NAME}(\"{}\")", sig.replace("\"", "\\\""));
|
||||
|
||||
let decrypted_string = match ctx.eval::<String,String>(call_string.clone()) {
|
||||
Ok(x) => x,
|
||||
@ -263,11 +252,7 @@ pub async fn process_decrypt_signature<W>(
|
||||
|
||||
let sig_function_name = &player_info.sig_function_name;
|
||||
|
||||
let mut call_string: String = String::new();
|
||||
call_string += sig_function_name;
|
||||
call_string += "(\"";
|
||||
call_string += &sig.replace("\"", "\\\"");
|
||||
call_string += "\")";
|
||||
let call_string = format!("{sig_function_name}(\"{}\")", sig.replace("\"", "\\\""));
|
||||
|
||||
drop(player_info);
|
||||
|
||||
|
54
src/main.rs
54
src/main.rs
@ -5,11 +5,12 @@ mod player;
|
||||
|
||||
use ::futures::StreamExt;
|
||||
use consts::{DEFAULT_SOCK_PATH, DEFAULT_TCP_URL};
|
||||
use env_logger::Env;
|
||||
use jobs::{process_decrypt_n_signature, process_fetch_update, GlobalState, JobOpcode};
|
||||
use log::{debug, error, info};
|
||||
use opcode::OpcodeDecoder;
|
||||
use player::fetch_update;
|
||||
use std::{env::args, sync::Arc};
|
||||
use env_logger::Env;
|
||||
use tokio::{
|
||||
fs::remove_file,
|
||||
io::{AsyncReadExt, AsyncWrite},
|
||||
@ -17,7 +18,6 @@ use tokio::{
|
||||
sync::Mutex,
|
||||
};
|
||||
use tokio_util::codec::Framed;
|
||||
use log::{info, error, debug};
|
||||
|
||||
use crate::jobs::{
|
||||
process_decrypt_signature, process_get_signature_timestamp, process_player_status,
|
||||
@ -30,7 +30,7 @@ macro_rules! loop_main {
|
||||
match fetch_update($s.clone()).await {
|
||||
Ok(()) => info!("Successfully fetched player"),
|
||||
Err(x) => {
|
||||
error!("Error occured while trying to fetch the player: {:?}", x);
|
||||
error!("Error occurred while trying to fetch the player: {:?}", x);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
@ -48,19 +48,14 @@ async fn main() {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
let args: Vec<String> = args().collect();
|
||||
let socket_url: &str = match args.get(1) {
|
||||
Some(stringref) => stringref,
|
||||
None => DEFAULT_SOCK_PATH,
|
||||
};
|
||||
let socket_url: &str = args.get(1).map(String::as_ref).unwrap_or(DEFAULT_SOCK_PATH);
|
||||
|
||||
// have to please rust
|
||||
let state: Arc<GlobalState> = Arc::new(GlobalState::new());
|
||||
|
||||
if socket_url == "--tcp" {
|
||||
let socket_tcp_url: &str = match args.get(2) {
|
||||
Some(stringref) => stringref,
|
||||
None => DEFAULT_TCP_URL,
|
||||
};
|
||||
let socket_tcp_url = args.get(2).map(String::as_ref).unwrap_or(DEFAULT_TCP_URL);
|
||||
|
||||
let tcp_socket = match TcpListener::bind(socket_tcp_url).await {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
@ -68,27 +63,29 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop_main!(tcp_socket, state);
|
||||
} else if socket_url == "--test" {
|
||||
// TODO: test the API aswell, this only tests the player script extractor
|
||||
// TODO: test the API as well, this only tests the player script extractor
|
||||
info!("Fetching player");
|
||||
match fetch_update(state.clone()).await {
|
||||
Ok(()) => std::process::exit(0),
|
||||
Err(_x) => std::process::exit(-1),
|
||||
}
|
||||
|
||||
std::process::exit(match fetch_update(state.clone()).await {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
});
|
||||
} else {
|
||||
let unix_socket = match UnixListener::bind(socket_url) {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
if x.kind() == std::io::ErrorKind::AddrInUse {
|
||||
remove_file(socket_url).await;
|
||||
Err(x) if x.kind() == std::io::ErrorKind::AddrInUse => {
|
||||
let _ = remove_file(socket_url).await;
|
||||
UnixListener::bind(socket_url).unwrap()
|
||||
} else {
|
||||
}
|
||||
Err(x) => {
|
||||
error!("Error occurred while trying to bind: {}", x);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop_main!(unix_socket, state);
|
||||
}
|
||||
}
|
||||
@ -108,18 +105,17 @@ where
|
||||
Ok(opcode) => {
|
||||
debug!("Received job: {}", opcode.opcode);
|
||||
|
||||
match opcode.opcode {
|
||||
JobOpcode::ForceUpdate => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
|
||||
match opcode.opcode {
|
||||
JobOpcode::ForceUpdate => {
|
||||
tokio::spawn(async move {
|
||||
process_fetch_update(cloned_state, cloned_sink, opcode.request_id)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
JobOpcode::DecryptNSignature => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
tokio::spawn(async move {
|
||||
process_decrypt_n_signature(
|
||||
cloned_state,
|
||||
@ -131,8 +127,6 @@ where
|
||||
});
|
||||
}
|
||||
JobOpcode::DecryptSignature => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
tokio::spawn(async move {
|
||||
process_decrypt_signature(
|
||||
cloned_state,
|
||||
@ -144,8 +138,6 @@ where
|
||||
});
|
||||
}
|
||||
JobOpcode::GetSignatureTimestamp => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
tokio::spawn(async move {
|
||||
process_get_signature_timestamp(
|
||||
cloned_state,
|
||||
@ -156,16 +148,12 @@ where
|
||||
});
|
||||
}
|
||||
JobOpcode::PlayerStatus => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
tokio::spawn(async move {
|
||||
process_player_status(cloned_state, cloned_sink, opcode.request_id)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
JobOpcode::PlayerUpdateTimestamp => {
|
||||
let cloned_state = state.clone();
|
||||
let cloned_sink = arc_sink.clone();
|
||||
tokio::spawn(async move {
|
||||
process_player_update_timestamp(
|
||||
cloned_state,
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::io::ErrorKind;
|
||||
use log::debug;
|
||||
use std::io::ErrorKind;
|
||||
use tokio_util::{
|
||||
bytes::{Buf, BufMut},
|
||||
codec::{Decoder, Encoder},
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
use log::{debug, error, info, warn};
|
||||
use regex::Regex;
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use crate::{
|
||||
consts::{
|
||||
@ -64,19 +64,20 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
let mut nsig_function_array_opt = None;
|
||||
// Extract nsig function array code
|
||||
for (index, nsig_function_array_str) in NSIG_FUNCTION_ARRAYS.iter().enumerate() {
|
||||
let nsig_function_array_regex = Regex::new(&nsig_function_array_str).unwrap();
|
||||
let nsig_function_array_regex = Regex::new(nsig_function_array_str).unwrap();
|
||||
nsig_function_array_opt = match nsig_function_array_regex.captures(&player_javascript) {
|
||||
None => {
|
||||
warn!("nsig function array did not work: {}", nsig_function_array_str);
|
||||
warn!(
|
||||
"nsig function array did not work: {}",
|
||||
nsig_function_array_str
|
||||
);
|
||||
if index == NSIG_FUNCTION_ARRAYS.len() {
|
||||
error!("!!ERROR!! nsig function array unable to be extracted");
|
||||
return Err(FetchUpdateStatus::NsigRegexCompileFailed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Some(i) => {
|
||||
Some(i)
|
||||
}
|
||||
Some(i) => Some(i),
|
||||
};
|
||||
break;
|
||||
}
|
||||
@ -90,10 +91,10 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
|
||||
let mut nsig_array_context_regex: String = String::new();
|
||||
nsig_array_context_regex += "var ";
|
||||
nsig_array_context_regex += &nsig_array_name.replace("$", "\\$");
|
||||
nsig_array_context_regex += "\\s*=\\s*\\[(.+?)][;,]";
|
||||
let nsig_array_context_regex: String = format!(
|
||||
"var {name}\\s*=\\s*\\[(.+?)][;,]",
|
||||
name = nsig_array_name.replace("$", "\\$")
|
||||
);
|
||||
|
||||
let nsig_array_context = match Regex::new(&nsig_array_context_regex) {
|
||||
Ok(x) => x,
|
||||
@ -115,20 +116,19 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
|
||||
let nsig_function_name = array_values.get(nsig_array_value).unwrap();
|
||||
|
||||
let mut nsig_function_code = String::new();
|
||||
nsig_function_code += "function ";
|
||||
nsig_function_code += NSIG_FUNCTION_NAME;
|
||||
let mut nsig_function_code = format!("function {NSIG_FUNCTION_NAME}");
|
||||
|
||||
// Extract nsig function code
|
||||
for (index, ending) in NSIG_FUNCTION_ENDINGS.iter().enumerate() {
|
||||
let mut nsig_function_code_regex_str: String = String::new();
|
||||
nsig_function_code_regex_str += &nsig_function_name.replace("$", "\\$");
|
||||
nsig_function_code_regex_str += ending;
|
||||
let nsig_function_code_regex_str =
|
||||
format!("{}{ending}", nsig_function_name.replace("$", "\\$"));
|
||||
|
||||
let nsig_function_code_regex = Regex::new(&nsig_function_code_regex_str).unwrap();
|
||||
|
||||
nsig_function_code += match nsig_function_code_regex.captures(&player_javascript) {
|
||||
None => {
|
||||
warn!("nsig function ending did not work: {}", ending);
|
||||
|
||||
if index == NSIG_FUNCTION_ENDINGS.len() {
|
||||
error!("!!ERROR!! nsig function unable to be extracted");
|
||||
return Err(FetchUpdateStatus::NsigRegexCompileFailed);
|
||||
@ -136,9 +136,7 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
|
||||
continue;
|
||||
}
|
||||
Some(i) => {
|
||||
i.get(1).unwrap().as_str()
|
||||
}
|
||||
Some(i) => i.get(1).unwrap().as_str(),
|
||||
};
|
||||
debug!("got nsig fn code: {}", nsig_function_code);
|
||||
break;
|
||||
@ -152,9 +150,10 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
let mut sig_function_body_regex_str: String = String::new();
|
||||
sig_function_body_regex_str += &sig_function_name.replace("$", "\\$");
|
||||
sig_function_body_regex_str += "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\}";
|
||||
let sig_function_body_regex_str = format!(
|
||||
"{name}=function\\([a-zA-Z0-9_]+\\)\\{{.+?\\}}",
|
||||
name = sig_function_name.replace("$", "\\$")
|
||||
);
|
||||
|
||||
let sig_function_body_regex = Regex::new(&sig_function_body_regex_str).unwrap();
|
||||
|
||||
@ -173,10 +172,8 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
let mut helper_object_body_regex_str = String::new();
|
||||
helper_object_body_regex_str += "(var ";
|
||||
helper_object_body_regex_str += helper_object_name;
|
||||
helper_object_body_regex_str += "=\\{(?:.|\\n)+?\\}\\};)";
|
||||
let helper_object_body_regex_str =
|
||||
format!("(var {helper_object_name}=\\{{(?:.|\\n)+?\\}}\\}};)");
|
||||
|
||||
let helper_object_body_regex = Regex::new(&helper_object_body_regex_str).unwrap();
|
||||
let helper_object_body = helper_object_body_regex
|
||||
@ -186,13 +183,7 @@ pub async fn fetch_update(state: Arc<GlobalState>) -> Result<(), FetchUpdateStat
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
let mut sig_code = String::new();
|
||||
sig_code += "var ";
|
||||
sig_code += sig_function_name;
|
||||
sig_code += ";";
|
||||
|
||||
sig_code += helper_object_body;
|
||||
sig_code += sig_function_body;
|
||||
let sig_code = format!("var {sig_function_name};{helper_object_body}{sig_function_body}");
|
||||
|
||||
info!("sig code: {}", sig_code);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user