Compare commits

...

3 Commits

Author SHA1 Message Date
5d9fdd1719 remove crosscompile, add wrac 2025-06-17 05:17:09 +03:00
2accb6e73d fix -r and -s with other args 2025-06-17 01:46:39 +03:00
c1e9d00d3a rustfmt 2025-06-17 00:03:25 +03:00
11 changed files with 823 additions and 506 deletions

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}

View File

@ -84,13 +84,12 @@ messages starting with a slash are sent to chat only if the `--disable-commands`
- [Message formats](docs/message_formats.md) - [Message formats](docs/message_formats.md)
- [Authenticated mode](docs/auth_mode.md) - [Authenticated mode](docs/auth_mode.md)
- [Cross compile](docs/cross_compile.md) - [WRAC protocol (v2.0)](docs/wrac.md)
- [FAQ](docs/faq.md) - [FAQ](docs/faq.md)
## see also ## see also
- [RAC-Hub - all about RAC protocol](https://the-stratosphere-solutions.github.io/RAC-Hub/) - [RAC-Hub - all about RAC protocol](https://the-stratosphere-solutions.github.io/RAC-Hub/)
- [WRAC protocol (v2.0)](docs/wrac.md)
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol) - [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
- [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab) - [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
- [Mefidroniy - client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client) - [Mefidroniy - client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client)

View File

@ -1,38 +1,69 @@
use clap::Parser;
use serde_default::DefaultFromSerde;
use serde_yml;
use std::str::FromStr; use std::str::FromStr;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use serde_yml;
use serde_default::DefaultFromSerde;
use clap::Parser;
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}"; const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
fn default_true() -> bool { true } fn default_true() -> bool {
pub fn default_max_messages() -> usize { 200 } true
pub fn default_update_time() -> usize { 100 } }
pub fn default_oof_update_time() -> usize { 10000 } pub fn default_max_messages() -> usize {
pub fn default_konata_size() -> usize { 100 } 200
pub fn default_host() -> String { "meex.lol:11234".to_string() } }
pub fn default_message_format() -> String { MESSAGE_FORMAT.to_string() } pub fn default_update_time() -> usize {
100
}
pub fn default_oof_update_time() -> usize {
10000
}
pub fn default_konata_size() -> usize {
100
}
pub fn default_host() -> String {
"meex.lol:11234".to_string()
}
pub fn default_message_format() -> String {
MESSAGE_FORMAT.to_string()
}
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)] #[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
pub struct Config { pub struct Config {
#[serde(default = "default_host")] pub host: String, #[serde(default = "default_host")]
#[serde(default)] pub name: Option<String>, pub host: String,
#[serde(default = "default_message_format")] pub message_format: String, #[serde(default)]
#[serde(default = "default_update_time")] pub update_time: usize, pub name: Option<String>,
#[serde(default = "default_oof_update_time")] pub oof_update_time: usize, #[serde(default = "default_message_format")]
#[serde(default = "default_max_messages")] pub max_messages: usize, pub message_format: String,
#[serde(default = "default_konata_size")] pub konata_size: usize, #[serde(default = "default_update_time")]
#[serde(default)] pub remove_gui_shit: bool, pub update_time: usize,
#[serde(default = "default_true")] pub hide_my_ip: bool, #[serde(default = "default_oof_update_time")]
#[serde(default)] pub show_other_ip: bool, pub oof_update_time: usize,
#[serde(default)] pub auth_enabled: bool, #[serde(default = "default_max_messages")]
#[serde(default = "default_true")] pub chunked_enabled: bool, pub max_messages: usize,
#[serde(default = "default_true")] pub formatting_enabled: bool, #[serde(default = "default_konata_size")]
#[serde(default = "default_true")] pub commands_enabled: bool, pub konata_size: usize,
#[serde(default)] pub proxy: Option<String>, #[serde(default)]
#[serde(default = "default_true")] pub notifications_enabled: bool, pub remove_gui_shit: bool,
#[serde(default)] pub debug_logs: bool, #[serde(default = "default_true")]
pub hide_my_ip: bool,
#[serde(default)]
pub show_other_ip: bool,
#[serde(default)]
pub auth_enabled: bool,
#[serde(default = "default_true")]
pub chunked_enabled: bool,
#[serde(default = "default_true")]
pub formatting_enabled: bool,
#[serde(default = "default_true")]
pub commands_enabled: bool,
#[serde(default)]
pub proxy: Option<String>,
#[serde(default = "default_true")]
pub notifications_enabled: bool,
#[serde(default)]
pub debug_logs: bool,
} }
pub fn get_config_path() -> PathBuf { pub fn get_config_path() -> PathBuf {
@ -98,54 +129,105 @@ pub fn save_config(path: PathBuf, config: &Config) {
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Args { pub struct Args {
/// Print config path /// Print config path
#[arg(short='p', long)] #[arg(short = 'p', long)]
pub config_path: bool, pub config_path: bool,
/// Print unformatted messages from chat and exit /// Print unformatted messages from chat and exit
#[arg(short='r', long)] #[arg(short = 'r', long)]
pub read_messages: bool, pub read_messages: bool,
/// Send unformatted message to chat and exit /// Send unformatted message to chat and exit
#[arg(short='s', long, value_name="MESSAGE")] #[arg(short = 's', long, value_name = "MESSAGE")]
pub send_message: Option<String>, pub send_message: Option<String>,
#[arg(short='H', long)] pub host: Option<String>, #[arg(short = 'H', long)]
#[arg(short='n', long)] pub name: Option<String>, pub host: Option<String>,
#[arg(long)] pub message_format: Option<String>, #[arg(short = 'n', long)]
#[arg(long)] pub update_time: Option<usize>, pub name: Option<String>,
#[arg(long)] pub oof_update_time: Option<usize>, #[arg(long)]
#[arg(long)] pub max_messages: Option<usize>, pub message_format: Option<String>,
#[arg(long)] pub konata_size: Option<usize>, #[arg(long)]
#[arg(long)] pub hide_my_ip: Option<bool>, pub update_time: Option<usize>,
#[arg(long)] pub show_other_ip: Option<bool>, #[arg(long)]
#[arg(long)] pub auth_enabled:Option <bool>, pub oof_update_time: Option<usize>,
#[arg(long)] pub remove_gui_shit: Option<bool>, #[arg(long)]
#[arg(long)] pub chunked_enabled: Option<bool>, pub max_messages: Option<usize>,
#[arg(long)] pub formatting_enabled: Option<bool>, #[arg(long)]
#[arg(long)] pub commands_enabled: Option<bool>, pub konata_size: Option<usize>,
#[arg(long)] pub notifications_enabled: Option<bool>, #[arg(long)]
#[arg(long)] pub proxy: Option<String>, pub hide_my_ip: Option<bool>,
#[arg(long)] pub debug_logs: bool, #[arg(long)]
pub show_other_ip: Option<bool>,
#[arg(long)]
pub auth_enabled: Option<bool>,
#[arg(long)]
pub remove_gui_shit: Option<bool>,
#[arg(long)]
pub chunked_enabled: Option<bool>,
#[arg(long)]
pub formatting_enabled: Option<bool>,
#[arg(long)]
pub commands_enabled: Option<bool>,
#[arg(long)]
pub notifications_enabled: Option<bool>,
#[arg(long)]
pub proxy: Option<String>,
#[arg(long)]
pub debug_logs: bool,
} }
impl Args { impl Args {
pub fn patch_config(&self, config: &mut Config) { pub fn patch_config(&self, config: &mut Config) {
if let Some(v) = self.host.clone() { config.host = v } if let Some(v) = self.host.clone() {
if let Some(v) = self.name.clone() { config.name = Some(v) } config.host = v
if let Some(v) = self.proxy.clone() { config.proxy = Some(v) } }
if let Some(v) = self.message_format.clone() { config.message_format = v } if let Some(v) = self.name.clone() {
if let Some(v) = self.update_time { config.update_time = v } config.name = Some(v)
if let Some(v) = self.oof_update_time { config.oof_update_time = v } }
if let Some(v) = self.max_messages { config.max_messages = v } if let Some(v) = self.proxy.clone() {
if let Some(v) = self.konata_size { config.konata_size = v } config.proxy = Some(v)
if let Some(v) = self.hide_my_ip { config.hide_my_ip = v } }
if let Some(v) = self.show_other_ip { config.show_other_ip = v } if let Some(v) = self.message_format.clone() {
if let Some(v) = self.remove_gui_shit { config.remove_gui_shit = v } config.message_format = v
if let Some(v) = self.auth_enabled { config.auth_enabled = v } }
if let Some(v) = self.chunked_enabled { config.chunked_enabled = v } if let Some(v) = self.update_time {
if let Some(v) = self.formatting_enabled { config.formatting_enabled = v } config.update_time = v
if let Some(v) = self.commands_enabled { config.commands_enabled = v } }
if let Some(v) = self.notifications_enabled { config.notifications_enabled = v } if let Some(v) = self.oof_update_time {
if self.debug_logs { config.debug_logs = true } config.oof_update_time = v
}
if let Some(v) = self.max_messages {
config.max_messages = v
}
if let Some(v) = self.konata_size {
config.konata_size = v
}
if let Some(v) = self.hide_my_ip {
config.hide_my_ip = v
}
if let Some(v) = self.show_other_ip {
config.show_other_ip = v
}
if let Some(v) = self.remove_gui_shit {
config.remove_gui_shit = v
}
if let Some(v) = self.auth_enabled {
config.auth_enabled = v
}
if let Some(v) = self.chunked_enabled {
config.chunked_enabled = v
}
if let Some(v) = self.formatting_enabled {
config.formatting_enabled = v
}
if let Some(v) = self.commands_enabled {
config.commands_enabled = v
}
if let Some(v) = self.notifications_enabled {
config.notifications_enabled = v
}
if self.debug_logs {
config.debug_logs = true
}
} }
} }

View File

@ -1,4 +1,8 @@
use std::sync::{atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc::Sender, Arc, RwLock}; use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc::Sender,
Arc, RwLock,
};
use rand::random; use rand::random;
@ -11,7 +15,7 @@ pub struct Context {
pub messages: RwLock<Vec<String>>, pub messages: RwLock<Vec<String>>,
pub packet_size: AtomicUsize, pub packet_size: AtomicUsize,
pub name: RwLock<String>, pub name: RwLock<String>,
pub is_focused: AtomicBool pub is_focused: AtomicBool,
} }
impl Context { impl Context {
@ -22,8 +26,13 @@ impl Context {
sender: RwLock::new(None), sender: RwLock::new(None),
messages: RwLock::new(Vec::new()), messages: RwLock::new(Vec::new()),
packet_size: AtomicUsize::default(), packet_size: AtomicUsize::default(),
name: RwLock::new(config.name.clone().unwrap_or_else(|| format!("Anon#{:X}", random::<u16>()))), name: RwLock::new(
is_focused: AtomicBool::new(true) config
.name
.clone()
.unwrap_or_else(|| format!("Anon#{:X}", random::<u16>())),
),
is_focused: AtomicBool::new(true),
} }
} }
@ -33,13 +42,16 @@ impl Context {
pub fn set_config(&self, config: &Config) { pub fn set_config(&self, config: &Config) {
*self.config.write().unwrap() = config.clone(); *self.config.write().unwrap() = config.clone();
*self.name.write().unwrap() = config.name.clone().unwrap_or_else(|| format!("Anon#{:X}", random::<u16>())); *self.name.write().unwrap() = config
.name
.clone()
.unwrap_or_else(|| format!("Anon#{:X}", random::<u16>()));
*self.registered.write().unwrap() = None; *self.registered.write().unwrap() = None;
*self.messages.write().unwrap() = Vec::new(); *self.messages.write().unwrap() = Vec::new();
self.packet_size.store(0, Ordering::SeqCst); self.packet_size.store(0, Ordering::SeqCst);
} }
pub fn config<T>(&self, map: fn (&Config) -> T) -> T { pub fn config<T>(&self, map: fn(&Config) -> T) -> T {
map(&self.config.read().unwrap()) map(&self.config.read().unwrap())
} }
@ -51,7 +63,12 @@ impl Context {
self.messages.read().unwrap().clone() self.messages.read().unwrap().clone()
} }
pub fn put_messages_packet(&self, max_length: usize, messages: Vec<String>, packet_size: usize) { pub fn put_messages_packet(
&self,
max_length: usize,
messages: Vec<String>,
packet_size: usize,
) {
self.packet_size.store(packet_size, Ordering::SeqCst); self.packet_size.store(packet_size, Ordering::SeqCst);
let mut messages = messages; let mut messages = messages;
if messages.len() > max_length { if messages.len() > max_length {
@ -60,7 +77,12 @@ impl Context {
*self.messages.write().unwrap() = messages; *self.messages.write().unwrap() = messages;
} }
pub fn add_messages_packet(&self, max_length: usize, messages: Vec<String>, packet_size: usize) { pub fn add_messages_packet(
&self,
max_length: usize,
messages: Vec<String>,
packet_size: usize,
) {
self.packet_size.store(packet_size, Ordering::SeqCst); self.packet_size.store(packet_size, Ordering::SeqCst);
self.add_message(max_length, messages); self.add_message(max_length, messages);
} }
@ -75,10 +97,10 @@ impl Context {
#[macro_export] #[macro_export]
macro_rules! connect_rac { macro_rules! connect_rac {
($ctx:ident) => { ($ctx:ident) => {
&mut connect( &mut connect(
&$ctx.config(|o| o.host.clone()), &$ctx.config(|o| o.host.clone()),
$ctx.config(|o| o.proxy.clone()) $ctx.config(|o| o.proxy.clone()),
)? )?
}; };
} }

View File

@ -1,34 +1,37 @@
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
use std::cell::RefCell; use std::cell::RefCell;
use std::time::{Duration, SystemTime};
use std::thread;
use std::error::Error; use std::error::Error;
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
use std::thread;
use std::time::{Duration, SystemTime};
use chrono::Local; use chrono::Local;
use gtk4::{self as gtk}; use gtk4::{self as gtk};
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::prelude::*;
use gtk::gdk::{Cursor, Display, Texture}; use gtk::gdk::{Cursor, Display, Texture};
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu}; use gtk::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu};
use gtk::glib::clone; use gtk::glib::clone;
use gtk::glib::{ use gtk::glib::{
self, clone::Downgrade, self, clone::Downgrade, source::timeout_add_local_once, timeout_add_local, timeout_add_once,
timeout_add_local,
source::timeout_add_local_once,
ControlFlow, ControlFlow,
timeout_add_once
}; };
use gtk::pango::WrapMode; use gtk::pango::WrapMode;
use gtk::prelude::*;
use gtk::{ use gtk::{
AboutDialog, Align, Application, ApplicationWindow, Box as GtkBox, AboutDialog, Align, Application, ApplicationWindow, Box as GtkBox, Button, Calendar,
Button, Calendar, CheckButton, CssProvider, Entry, Fixed, GestureClick, CheckButton, CssProvider, Entry, Fixed, GestureClick, Justification, Label, ListBox,
Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings, Window Orientation, Overlay, Picture, ScrolledWindow, Settings, Window,
}; };
use super::{config::{default_max_messages, default_update_time, default_konata_size, default_oof_update_time, get_config_path, save_config, Config}, use super::{
ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize_message, SERVER_LIST}; config::{
default_konata_size, default_max_messages, default_oof_update_time, default_update_time,
get_config_path, save_config, Config,
},
ctx::Context,
on_send_message, parse_message, print_message, recv_tick, sanitize_message, SERVER_LIST,
};
struct UiModel { struct UiModel {
is_dark_theme: bool, is_dark_theme: bool,
@ -39,7 +42,7 @@ struct UiModel {
#[cfg(feature = "libnotify")] #[cfg(feature = "libnotify")]
notifications: Arc<RwLock<Vec<libnotify::Notification>>>, notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
#[cfg(not(feature = "libnotify"))] #[cfg(not(feature = "libnotify"))]
notifications: Arc<RwLock<Vec<String>>> notifications: Arc<RwLock<Vec<String>>>,
} }
thread_local!( thread_local!(
@ -47,11 +50,23 @@ thread_local!(
); );
pub fn clear_chat_messages(ctx: Arc<Context>, messages: Vec<String>) { pub fn clear_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
let _ = ctx.sender.read().unwrap().clone().unwrap().send((messages, true)); let _ = ctx
.sender
.read()
.unwrap()
.clone()
.unwrap()
.send((messages, true));
} }
pub fn add_chat_messages(ctx: Arc<Context>, messages: Vec<String>) { pub fn add_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
let _ = ctx.sender.read().unwrap().clone().unwrap().send((messages, false)); let _ = ctx
.sender
.read()
.unwrap()
.clone()
.unwrap()
.send((messages, false));
} }
fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> { fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> {
@ -62,97 +77,83 @@ fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> {
} }
macro_rules! gui_entry_setting { macro_rules! gui_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
{ let hbox = GtkBox::new(Orientation::Horizontal, 5);
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder() hbox.append(&Label::builder().label($e).build());
.label($e)
.build());
let entry = Entry::builder() let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone())) .text(&$ctx.config(|o| o.$i.clone()))
.build(); .build();
hbox.append(&entry); hbox.append(&entry);
$vbox.append(&hbox); $vbox.append(&hbox);
entry entry
} }};
};
} }
macro_rules! gui_usize_entry_setting { macro_rules! gui_usize_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
{ let hbox = GtkBox::new(Orientation::Horizontal, 5);
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder() hbox.append(&Label::builder().label($e).build());
.label($e)
.build());
let entry = Entry::builder() let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.to_string())) .text(&$ctx.config(|o| o.$i.to_string()))
.build(); .build();
hbox.append(&entry); hbox.append(&entry);
$vbox.append(&hbox); $vbox.append(&hbox);
entry entry
} }};
};
} }
macro_rules! gui_option_entry_setting { macro_rules! gui_option_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
{ let hbox = GtkBox::new(Orientation::Horizontal, 5);
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder() hbox.append(&Label::builder().label($e).build());
.label($e)
.build());
let entry = Entry::builder() let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default()) .text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default())
.build(); .build();
hbox.append(&entry); hbox.append(&entry);
$vbox.append(&hbox); $vbox.append(&hbox);
entry entry
} }};
};
} }
macro_rules! gui_checkbox_setting { macro_rules! gui_checkbox_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
{ let hbox = GtkBox::new(Orientation::Horizontal, 5);
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder() hbox.append(&Label::builder().label($e).build());
.label($e)
.build());
let entry = CheckButton::builder() let entry = CheckButton::builder().active($ctx.config(|o| o.$i)).build();
.active($ctx.config(|o| o.$i))
.build();
hbox.append(&entry); hbox.append(&entry);
$vbox.append(&hbox); $vbox.append(&hbox);
entry entry
} }};
};
} }
fn update_window_title(ctx: Arc<Context>) { fn update_window_title(ctx: Arc<Context>) {
GLOBAL.with(|global| { GLOBAL.with(|global| {
if let Some(ui) = &*global.borrow() { if let Some(ui) = &*global.borrow() {
ui.window.set_title(Some(&format!("bRAC - Connected to {} as {}", ctx.config(|o| o.host.clone()), &ctx.name()))) ui.window.set_title(Some(&format!(
"bRAC - Connected to {} as {}",
ctx.config(|o| o.host.clone()),
&ctx.name()
)))
} }
}) })
} }
@ -169,21 +170,41 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
let host_entry = gui_entry_setting!("Host", host, ctx, settings_vbox); let host_entry = gui_entry_setting!("Host", host, ctx, settings_vbox);
let name_entry = gui_option_entry_setting!("Name", name, ctx, settings_vbox); let name_entry = gui_option_entry_setting!("Name", name, ctx, settings_vbox);
let message_format_entry = gui_entry_setting!("Message Format", message_format, ctx, settings_vbox); let message_format_entry =
gui_entry_setting!("Message Format", message_format, ctx, settings_vbox);
let proxy_entry = gui_option_entry_setting!("Socks5 proxy", proxy, ctx, settings_vbox); let proxy_entry = gui_option_entry_setting!("Socks5 proxy", proxy, ctx, settings_vbox);
let update_time_entry = gui_usize_entry_setting!("Update Time", update_time, ctx, settings_vbox); let update_time_entry =
let oof_update_time_entry = gui_usize_entry_setting!("Out-of-focus Update Time", oof_update_time, ctx, settings_vbox); gui_usize_entry_setting!("Update Time", update_time, ctx, settings_vbox);
let max_messages_entry = gui_usize_entry_setting!("Max Messages", max_messages, ctx, settings_vbox); let oof_update_time_entry = gui_usize_entry_setting!(
"Out-of-focus Update Time",
oof_update_time,
ctx,
settings_vbox
);
let max_messages_entry =
gui_usize_entry_setting!("Max Messages", max_messages, ctx, settings_vbox);
let hide_my_ip_entry = gui_checkbox_setting!("Hide My IP", hide_my_ip, ctx, settings_vbox); let hide_my_ip_entry = gui_checkbox_setting!("Hide My IP", hide_my_ip, ctx, settings_vbox);
let show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, settings_vbox); let show_other_ip_entry =
let auth_enabled_entry = gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, settings_vbox); gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, settings_vbox);
let chunked_enabled_entry = gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, settings_vbox); let auth_enabled_entry =
let formatting_enabled_entry = gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, settings_vbox); gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, settings_vbox);
let commands_enabled_entry = gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, settings_vbox); let chunked_enabled_entry =
let notifications_enabled_entry = gui_checkbox_setting!("Notifications Enabled", notifications_enabled, ctx, settings_vbox); gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, settings_vbox);
let formatting_enabled_entry =
gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, settings_vbox);
let commands_enabled_entry =
gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, settings_vbox);
let notifications_enabled_entry = gui_checkbox_setting!(
"Notifications Enabled",
notifications_enabled,
ctx,
settings_vbox
);
let debug_logs_entry = gui_checkbox_setting!("Debug Logs", debug_logs, ctx, settings_vbox); let debug_logs_entry = gui_checkbox_setting!("Debug Logs", debug_logs, ctx, settings_vbox);
let konata_size_entry = gui_usize_entry_setting!("Konata Size", konata_size, ctx, settings_vbox); let konata_size_entry =
let remove_gui_shit_entry = gui_checkbox_setting!("Remove Gui Shit", remove_gui_shit, ctx, settings_vbox); gui_usize_entry_setting!("Konata Size", konata_size, ctx, settings_vbox);
let remove_gui_shit_entry =
gui_checkbox_setting!("Remove Gui Shit", remove_gui_shit, ctx, settings_vbox);
let scrollable = ScrolledWindow::builder() let scrollable = ScrolledWindow::builder()
.child(&settings_vbox) .child(&settings_vbox)
@ -193,37 +214,53 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
vbox.append(&scrollable); vbox.append(&scrollable);
let save_button = Button::builder() let save_button = Button::builder().label("Save").build();
.label("Save")
.build();
vbox.append(&save_button); vbox.append(&save_button);
save_button.connect_clicked(clone!( save_button.connect_clicked(clone!(
#[weak] ctx, #[weak]
#[weak] host_entry, ctx,
#[weak] name_entry, #[weak]
#[weak] message_format_entry, host_entry,
#[weak] update_time_entry, #[weak]
#[weak] max_messages_entry, name_entry,
#[weak] hide_my_ip_entry, #[weak]
#[weak] show_other_ip_entry, message_format_entry,
#[weak] auth_enabled_entry, #[weak]
#[weak] chunked_enabled_entry, update_time_entry,
#[weak] formatting_enabled_entry, #[weak]
#[weak] commands_enabled_entry, max_messages_entry,
#[weak] notifications_enabled_entry, #[weak]
#[weak] proxy_entry, hide_my_ip_entry,
#[weak] debug_logs_entry, #[weak]
#[weak] oof_update_time_entry, show_other_ip_entry,
#[weak] konata_size_entry, #[weak]
#[weak] remove_gui_shit_entry, auth_enabled_entry,
#[weak]
chunked_enabled_entry,
#[weak]
formatting_enabled_entry,
#[weak]
commands_enabled_entry,
#[weak]
notifications_enabled_entry,
#[weak]
proxy_entry,
#[weak]
debug_logs_entry,
#[weak]
oof_update_time_entry,
#[weak]
konata_size_entry,
#[weak]
remove_gui_shit_entry,
move |_| { move |_| {
let config = Config { let config = Config {
host: host_entry.text().to_string(), host: host_entry.text().to_string(),
name: { name: {
let name = name_entry.text().to_string(); let name = name_entry.text().to_string();
if name.is_empty() { if name.is_empty() {
None None
} else { } else {
@ -233,7 +270,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
message_format: message_format_entry.text().to_string(), message_format: message_format_entry.text().to_string(),
update_time: { update_time: {
let update_time = update_time_entry.text(); let update_time = update_time_entry.text();
if let Ok(update_time) = update_time.parse::<usize>() { if let Ok(update_time) = update_time.parse::<usize>() {
update_time update_time
} else { } else {
@ -244,7 +281,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
}, },
oof_update_time: { oof_update_time: {
let oof_update_time = oof_update_time_entry.text(); let oof_update_time = oof_update_time_entry.text();
if let Ok(oof_update_time) = oof_update_time.parse::<usize>() { if let Ok(oof_update_time) = oof_update_time.parse::<usize>() {
oof_update_time oof_update_time
} else { } else {
@ -255,7 +292,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
}, },
konata_size: { konata_size: {
let konata_size = konata_size_entry.text(); let konata_size = konata_size_entry.text();
if let Ok(konata_size) = konata_size.parse::<usize>() { if let Ok(konata_size) = konata_size.parse::<usize>() {
konata_size.max(0).min(200) konata_size.max(0).min(200)
} else { } else {
@ -266,7 +303,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
}, },
max_messages: { max_messages: {
let max_messages = max_messages_entry.text(); let max_messages = max_messages_entry.text();
if let Ok(max_messages) = max_messages.parse::<usize>() { if let Ok(max_messages) = max_messages.parse::<usize>() {
max_messages max_messages
} else { } else {
@ -286,13 +323,13 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
debug_logs: debug_logs_entry.is_active(), debug_logs: debug_logs_entry.is_active(),
proxy: { proxy: {
let proxy = proxy_entry.text().to_string(); let proxy = proxy_entry.text().to_string();
if proxy.is_empty() { if proxy.is_empty() {
None None
} else { } else {
Some(proxy) Some(proxy)
} }
} },
}; };
ctx.set_config(&config); ctx.set_config(&config);
save_config(get_config_path(), &config); save_config(get_config_path(), &config);
@ -300,27 +337,39 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
} }
)); ));
let reset_button = Button::builder() let reset_button = Button::builder().label("Reset all").build();
.label("Reset all")
.build();
vbox.append(&reset_button); vbox.append(&reset_button);
reset_button.connect_clicked(clone!( reset_button.connect_clicked(clone!(
#[weak] ctx, #[weak]
#[weak] host_entry, ctx,
#[weak] name_entry, #[weak]
#[weak] message_format_entry, host_entry,
#[weak] update_time_entry, #[weak]
#[weak] max_messages_entry, name_entry,
#[weak] hide_my_ip_entry, #[weak]
#[weak] show_other_ip_entry, message_format_entry,
#[weak] auth_enabled_entry, #[weak]
#[weak] chunked_enabled_entry, update_time_entry,
#[weak] formatting_enabled_entry, #[weak]
#[weak] commands_enabled_entry, max_messages_entry,
#[weak] notifications_enabled_entry, #[weak]
#[weak] proxy_entry, hide_my_ip_entry,
#[weak]
show_other_ip_entry,
#[weak]
auth_enabled_entry,
#[weak]
chunked_enabled_entry,
#[weak]
formatting_enabled_entry,
#[weak]
commands_enabled_entry,
#[weak]
notifications_enabled_entry,
#[weak]
proxy_entry,
move |_| { move |_| {
let config = Config::default(); let config = Config::default();
ctx.set_config(&config); ctx.set_config(&config);
@ -365,7 +414,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
}); });
window.add_controller(controller); window.add_controller(controller);
window.present(); window.present();
} }
@ -387,7 +436,8 @@ fn build_menu(ctx: Arc<Context>, app: &Application) {
app.add_action_entries([ app.add_action_entries([
ActionEntry::builder("settings") ActionEntry::builder("settings")
.activate(clone!( .activate(clone!(
#[weak] ctx, #[weak]
ctx,
move |a: &Application, _, _| { move |a: &Application, _, _| {
open_settings(ctx, a); open_settings(ctx, a);
} }
@ -400,12 +450,14 @@ fn build_menu(ctx: Arc<Context>, app: &Application) {
.build(), .build(),
ActionEntry::builder("about") ActionEntry::builder("about")
.activate(clone!( .activate(clone!(
#[weak] app, #[weak]
app,
move |_, _, _| { move |_, _, _| {
AboutDialog::builder() AboutDialog::builder()
.application(&app) .application(&app)
.authors(["MeexReay"]) .authors(["MeexReay"])
.license(" DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE .license(
" DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004 Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
@ -417,24 +469,29 @@ fn build_menu(ctx: Arc<Context>, app: &Application) {
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.") 0. You just DO WHAT THE FUCK YOU WANT TO.",
)
.comments("better RAC client") .comments("better RAC client")
.website("https://github.com/MeexReay/bRAC") .website("https://github.com/MeexReay/bRAC")
.website_label("source code") .website_label("source code")
.logo(&Texture::for_pixbuf(&load_pixbuf(include_bytes!("images/icon.png")).unwrap())) .logo(&Texture::for_pixbuf(
&load_pixbuf(include_bytes!("images/icon.png")).unwrap(),
))
.build() .build()
.present(); .present();
} }
)) ))
.build() .build(),
]); ]);
} }
fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel { fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let is_dark_theme = if let Some(settings) = Settings::default() { let is_dark_theme = if let Some(settings) = Settings::default() {
settings.is_gtk_application_prefer_dark_theme() || settings.gtk_theme_name() settings.is_gtk_application_prefer_dark_theme()
.map(|o| o.to_lowercase().contains("dark")) || settings
.unwrap_or_default() .gtk_theme_name()
.map(|o| o.to_lowercase().contains("dark"))
.unwrap_or_default()
} else { } else {
false false
}; };
@ -452,11 +509,13 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let remove_gui_shit = ctx.config(|c| c.remove_gui_shit); let remove_gui_shit = ctx.config(|c| c.remove_gui_shit);
if !remove_gui_shit { if !remove_gui_shit {
widget_box.append(&Calendar::builder() widget_box.append(
.css_classes(["calendar"]) &Calendar::builder()
.show_heading(false) .css_classes(["calendar"])
.can_target(false) .show_heading(false)
.build()); .can_target(false)
.build(),
);
} }
let server_list_vbox = GtkBox::new(Orientation::Vertical, 5); let server_list_vbox = GtkBox::new(Orientation::Vertical, 5);
@ -466,15 +525,13 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
for url in SERVER_LIST.iter() { for url in SERVER_LIST.iter() {
let url = url.to_string(); let url = url.to_string();
let label = Label::builder() let label = Label::builder().label(&url).halign(Align::Start).build();
.label(&url)
.halign(Align::Start)
.build();
let click = GestureClick::new(); let click = GestureClick::new();
click.connect_pressed(clone!( click.connect_pressed(clone!(
#[weak] ctx, #[weak]
ctx,
move |_, _, _, _| { move |_, _, _, _| {
let mut config = ctx.config.read().unwrap().clone(); let mut config = ctx.config.read().unwrap().clone();
config.host = url.clone(); config.host = url.clone();
@ -501,10 +558,15 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let konata_size = ctx.config(|c| c.konata_size) as i32; let konata_size = ctx.config(|c| c.konata_size) as i32;
let konata = Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")).unwrap()); let konata =
Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")).unwrap());
konata.set_size_request(174 * konata_size / 100, 127 * konata_size / 100); konata.set_size_request(174 * konata_size / 100, 127 * konata_size / 100);
fixed.put(&konata, (499 - 174 * konata_size / 100) as f64, (131 - 127 * konata_size / 100) as f64); fixed.put(
&konata,
(499 - 174 * konata_size / 100) as f64,
(131 - 127 * konata_size / 100) as f64,
);
let logo_gif = include_bytes!("images/logo.gif"); let logo_gif = include_bytes!("images/logo.gif");
@ -512,11 +574,11 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
logo.set_size_request(152 * konata_size / 100, 64 * konata_size / 100); logo.set_size_request(152 * konata_size / 100, 64 * konata_size / 100);
let logo_anim = PixbufAnimation::from_stream( let logo_anim = PixbufAnimation::from_stream(
&MemoryInputStream::from_bytes( &MemoryInputStream::from_bytes(&glib::Bytes::from(logo_gif)),
&glib::Bytes::from(logo_gif) None::<&gio::Cancellable>,
), )
None::<&gio::Cancellable> .unwrap()
).unwrap().iter(Some(SystemTime::now())); .iter(Some(SystemTime::now()));
timeout_add_local(Duration::from_millis(30), { timeout_add_local(Duration::from_millis(30), {
let logo = logo.clone(); let logo = logo.clone();
@ -531,9 +593,13 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
ControlFlow::Continue ControlFlow::Continue
} }
}); });
// 262, 4 // 262, 4
fixed.put(&logo, (436 - 174 * konata_size / 100) as f64, (131 - 127 * konata_size / 100) as f64); fixed.put(
&logo,
(436 - 174 * konata_size / 100) as f64,
(131 - 127 * konata_size / 100) as f64,
);
let time = Label::builder() let time = Label::builder()
.label(&Local::now().format("%H:%M").to_string()) .label(&Local::now().format("%H:%M").to_string())
@ -555,7 +621,6 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
fixed.set_halign(Align::End); fixed.set_halign(Align::End);
widget_box_overlay.add_overlay(&fixed); widget_box_overlay.add_overlay(&fixed);
} }
widget_box_overlay.set_child(Some(&widget_box)); widget_box_overlay.set_child(Some(&widget_box));
@ -599,16 +664,24 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
.build(); .build();
send_btn.connect_clicked(clone!( send_btn.connect_clicked(clone!(
#[weak] text_entry, #[weak]
#[weak] ctx, text_entry,
#[weak]
ctx,
move |_| { move |_| {
if text_entry.text().is_empty() { return; } if text_entry.text().is_empty() {
timeout_add_local_once(Duration::ZERO, clone!( return;
#[weak] text_entry, }
move || { timeout_add_local_once(
text_entry.set_text(""); Duration::ZERO,
} clone!(
)); #[weak]
text_entry,
move || {
text_entry.set_text("");
}
),
);
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) { if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
if ctx.config(|o| o.debug_logs) { if ctx.config(|o| o.debug_logs) {
@ -620,16 +693,24 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
)); ));
text_entry.connect_activate(clone!( text_entry.connect_activate(clone!(
#[weak] text_entry, #[weak]
#[weak] ctx, text_entry,
#[weak]
ctx,
move |_| { move |_| {
if text_entry.text().is_empty() { return; } if text_entry.text().is_empty() {
timeout_add_local_once(Duration::ZERO, clone!( return;
#[weak] text_entry, }
move || { timeout_add_local_once(
text_entry.set_text(""); Duration::ZERO,
} clone!(
)); #[weak]
text_entry,
move || {
text_entry.set_text("");
}
),
);
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) { if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
if ctx.config(|o| o.debug_logs) { if ctx.config(|o| o.debug_logs) {
@ -648,17 +729,22 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
timeout_add_local_once(Duration::ZERO, { timeout_add_local_once(Duration::ZERO, {
let scrolled_window_weak = scrolled_window_weak.clone(); let scrolled_window_weak = scrolled_window_weak.clone();
move || { move || {
if let Some(o) = scrolled_window_weak.upgrade() { if let Some(o) = scrolled_window_weak.upgrade() {
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size()); o.vadjustment()
.set_value(o.vadjustment().upper() - o.vadjustment().page_size());
} }
} }
}); });
let window = ApplicationWindow::builder() let window = ApplicationWindow::builder()
.application(app) .application(app)
.title(format!("bRAC - Connected to {} as {}", ctx.config(|o| o.host.clone()), &ctx.name())) .title(format!(
"bRAC - Connected to {} as {}",
ctx.config(|o| o.host.clone()),
&ctx.name()
))
.default_width(500) .default_width(500)
.default_height(500) .default_height(500)
.resizable(true) .resizable(true)
@ -674,7 +760,8 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let scrolled_window_weak = scrolled_window_weak.clone(); let scrolled_window_weak = scrolled_window_weak.clone();
timeout_add_local_once(Duration::ZERO, move || { timeout_add_local_once(Duration::ZERO, move || {
if let Some(o) = scrolled_window_weak.upgrade() { if let Some(o) = scrolled_window_weak.upgrade() {
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size()); o.vadjustment()
.set_value(o.vadjustment().upper() - o.vadjustment().page_size());
} }
}); });
} }
@ -701,7 +788,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
*ctx.sender.write().unwrap() = Some(Arc::new(sender)); *ctx.sender.write().unwrap() = Some(Arc::new(sender));
run_recv_loop(ctx.clone()); run_recv_loop(ctx.clone());
ui.window.connect_notify(Some("is-active"), { ui.window.connect_notify(Some("is-active"), {
let ctx = ctx.clone(); let ctx = ctx.clone();
@ -765,7 +852,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
fn load_css(is_dark_theme: bool) { fn load_css(is_dark_theme: bool) {
let provider = CssProvider::new(); let provider = CssProvider::new();
provider.load_from_data(&format!( provider.load_from_data(&format!(
"{}\n{}", "{}\n{}",
if is_dark_theme { if is_dark_theme {
include_str!("styles/dark.css") include_str!("styles/dark.css")
} else { } else {
@ -788,7 +875,9 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
let notification = Notification::new(title, message, None); let notification = Notification::new(title, message, None);
notification.set_app_name("bRAC"); notification.set_app_name("bRAC");
let pixbuf_loader = gdk_pixbuf::PixbufLoader::new(); let pixbuf_loader = gdk_pixbuf::PixbufLoader::new();
pixbuf_loader.loader_write(include_bytes!("images/icon.png")).unwrap(); pixbuf_loader
.loader_write(include_bytes!("images/icon.png"))
.unwrap();
pixbuf_loader.close().unwrap(); pixbuf_loader.close().unwrap();
notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap()); notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap());
notification.show().expect("libnotify send error"); notification.show().expect("libnotify send error");
@ -798,7 +887,10 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
#[cfg(not(feature = "libnotify"))] #[cfg(not(feature = "libnotify"))]
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) { fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
use std::{hash::{DefaultHasher, Hasher}, time::UNIX_EPOCH}; use std::{
hash::{DefaultHasher, Hasher},
time::UNIX_EPOCH,
};
use gtk4::gio::Notification; use gtk4::gio::Notification;
@ -806,7 +898,14 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
hash.write(title.as_bytes()); hash.write(title.as_bytes());
hash.write(message.as_bytes()); hash.write(message.as_bytes());
let id = format!("bRAC-{}-{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(), hash.finish()); let id = format!(
"bRAC-{}-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
hash.finish()
);
let notif = Notification::new(title); let notif = Notification::new(title);
notif.set_body(Some(&message)); notif.set_body(Some(&message));
@ -816,7 +915,9 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
} }
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool) { fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool) {
let Some(message) = sanitize_message(message) else { return; }; let Some(message) = sanitize_message(message) else {
return;
};
if message.is_empty() { if message.is_empty() {
return; return;
@ -825,17 +926,9 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
// TODO: softcode these colors // TODO: softcode these colors
let (ip_color, date_color, text_color) = if ui.is_dark_theme { let (ip_color, date_color, text_color) = if ui.is_dark_theme {
( ("#494949", "#929292", "#FFFFFF")
"#494949",
"#929292",
"#FFFFFF"
)
} else { } else {
( ("#585858", "#292929", "#000000")
"#585858",
"#292929",
"#000000"
)
}; };
let mut label = String::new(); let mut label = String::new();
@ -843,18 +936,33 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
if let Some((date, ip, content, nick)) = parse_message(message.clone()) { if let Some((date, ip, content, nick)) = parse_message(message.clone()) {
if let Some(ip) = ip { if let Some(ip) = ip {
if ctx.config(|o| o.show_other_ip) { if ctx.config(|o| o.show_other_ip) {
label.push_str(&format!("<span color=\"{ip_color}\">{}</span> ", glib::markup_escape_text(&ip))); label.push_str(&format!(
"<span color=\"{ip_color}\">{}</span> ",
glib::markup_escape_text(&ip)
));
} }
} }
label.push_str(&format!("<span color=\"{date_color}\">[{}]</span> ", glib::markup_escape_text(&date))); label.push_str(&format!(
"<span color=\"{date_color}\">[{}]</span> ",
glib::markup_escape_text(&date)
));
if let Some((name, color)) = nick { if let Some((name, color)) = nick {
label.push_str(&format!("<span font_weight=\"bold\" color=\"{}\">&lt;{}&gt;</span> ", color.to_uppercase(), glib::markup_escape_text(&name))); label.push_str(&format!(
"<span font_weight=\"bold\" color=\"{}\">&lt;{}&gt;</span> ",
color.to_uppercase(),
glib::markup_escape_text(&name)
));
if notify && !ui.window.is_active() { if notify && !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) { if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, &format!("{}'s Message", &name), &glib::markup_escape_text(&content)); send_notification(
ctx.clone(),
ui,
&format!("{}'s Message", &name),
&glib::markup_escape_text(&content),
);
} }
} }
} else { } else {
@ -865,9 +973,15 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
} }
} }
label.push_str(&format!("<span color=\"{text_color}\">{}</span>", glib::markup_escape_text(&content))); label.push_str(&format!(
"<span color=\"{text_color}\">{}</span>",
glib::markup_escape_text(&content)
));
} else { } else {
label.push_str(&format!("<span color=\"{text_color}\">{}</span>", glib::markup_escape_text(&message))); label.push_str(&format!(
"<span color=\"{text_color}\">{}</span>",
glib::markup_escape_text(&message)
));
if notify && !ui.window.is_active() { if notify && !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) { if ctx.config(|o| o.chunked_enabled) {
@ -875,18 +989,20 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
} }
} }
} }
let hbox = GtkBox::new(Orientation::Horizontal, 2); let hbox = GtkBox::new(Orientation::Horizontal, 2);
hbox.append(&Label::builder() hbox.append(
.label(&label) &Label::builder()
.halign(Align::Start) .label(&label)
.valign(Align::Start) .halign(Align::Start)
.selectable(true) .valign(Align::Start)
.wrap(true) .selectable(true)
.wrap_mode(WrapMode::WordChar) .wrap(true)
.use_markup(true) .wrap_mode(WrapMode::WordChar)
.build()); .use_markup(true)
.build(),
);
hbox.set_hexpand(true); hbox.set_hexpand(true);
@ -896,7 +1012,8 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
GLOBAL.with(|global| { GLOBAL.with(|global| {
if let Some(ui) = &*global.borrow() { if let Some(ui) = &*global.borrow() {
let o = &ui.chat_scrolled; let o = &ui.chat_scrolled;
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size()); o.vadjustment()
.set_value(o.vadjustment().upper() - o.vadjustment().page_size());
} }
}); });
}); });
@ -905,25 +1022,26 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
fn make_recv_tick(ctx: Arc<Context>) { fn make_recv_tick(ctx: Arc<Context>) {
if let Err(e) = recv_tick(ctx.clone()) { if let Err(e) = recv_tick(ctx.clone()) {
if ctx.config(|o| o.debug_logs) { if ctx.config(|o| o.debug_logs) {
let _ = print_message(ctx.clone(), format!("Print messages error: {}", e.to_string()).to_string()); let _ = print_message(
ctx.clone(),
format!("Print messages error: {}", e.to_string()).to_string(),
);
} }
thread::sleep(Duration::from_secs(1)); thread::sleep(Duration::from_secs(1));
} }
} }
fn run_recv_loop(ctx: Arc<Context>) { fn run_recv_loop(ctx: Arc<Context>) {
thread::spawn(move || { thread::spawn(move || loop {
loop { make_recv_tick(ctx.clone());
make_recv_tick(ctx.clone());
thread::sleep(Duration::from_millis( thread::sleep(Duration::from_millis(
if ctx.is_focused.load(Ordering::SeqCst) { if ctx.is_focused.load(Ordering::SeqCst) {
ctx.config(|o| o.update_time) as u64 ctx.config(|o| o.update_time) as u64
} else { } else {
ctx.config(|o| o.oof_update_time) as u64 ctx.config(|o| o.oof_update_time) as u64
} },
)); ));
}
}); });
} }

View File

@ -1,10 +1,14 @@
use std::{ use std::{
error::Error, sync::Arc, time::{SystemTime, UNIX_EPOCH} error::Error,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
}; };
use crate::connect_rac; use crate::connect_rac;
use super::proto::{connect, read_messages, send_message, send_message_spoof_auth, register_user, send_message_auth}; use super::proto::{
connect, read_messages, register_user, send_message, send_message_auth, send_message_spoof_auth,
};
use gui::{add_chat_messages, clear_chat_messages}; use gui::{add_chat_messages, clear_chat_messages};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -14,7 +18,6 @@ use ctx::Context;
pub use gui::run_main_loop; pub use gui::run_main_loop;
const HELP_MESSAGE: &str = "Help message: const HELP_MESSAGE: &str = "Help message:
/help - show help message /help - show help message
/register password - register user /register password - register user
@ -38,16 +41,15 @@ lazy_static! {
]; ];
pub static ref SERVER_LIST: Vec<String> = vec![ pub static ref SERVER_LIST: Vec<String> = vec![
"rac://meex.lol".to_string(), "rac://meex.lol".to_string(),
"rac://meex.lol:11234".to_string(), "rac://meex.lol:11234".to_string(),
"rac://91.192.22.20".to_string() "rac://91.192.22.20".to_string()
]; ];
} }
pub mod gui;
pub mod config; pub mod config;
pub mod ctx; pub mod ctx;
pub mod gui;
pub fn sanitize_text(input: &str) -> String { pub fn sanitize_text(input: &str) -> String {
let without_ansi = ANSI_REGEX.replace_all(input, ""); let without_ansi = ANSI_REGEX.replace_all(input, "");
@ -56,8 +58,7 @@ pub fn sanitize_text(input: &str) -> String {
} }
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> { pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
for i in message.split("\n") for i in message.split("\n").map(|o| o.to_string()) {
.map(|o| o.to_string()) {
print_message(ctx.clone(), i)?; print_message(ctx.clone(), i)?;
} }
Ok(()) Ok(())
@ -69,45 +70,52 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
let args = args.split(" ").collect::<Vec<&str>>(); let args = args.split(" ").collect::<Vec<&str>>();
if command == "clear" { if command == "clear" {
let Some(times) = args.get(0) else { return Ok(()) }; let Some(times) = args.get(0) else {
return Ok(());
};
let times = times.parse()?; let times = times.parse()?;
for _ in 0..times { for _ in 0..times {
send_message(connect_rac!(ctx), "\r")?; send_message(connect_rac!(ctx), "\r")?;
} }
} else if command == "spam" { } else if command == "spam" {
let Some(times) = args.get(0) else { return Ok(()) }; let Some(times) = args.get(0) else {
return Ok(());
};
let times = times.parse()?; let times = times.parse()?;
let msg = args[1..].join(" "); let msg = args[1..].join(" ");
for _ in 0..times { for _ in 0..times {
send_message(connect_rac!(ctx), &("\r".to_string()+&msg))?; send_message(connect_rac!(ctx), &("\r".to_string() + &msg))?;
} }
} else if command == "help" { } else if command == "help" {
add_message(ctx.clone(), HELP_MESSAGE)?; add_message(ctx.clone(), HELP_MESSAGE)?;
} else if command == "register" { } else if command == "register" {
let Some(pass) = args.get(0) else { let Some(pass) = args.get(0) else {
add_message(ctx.clone(), "please provide password as the first argument")?; add_message(ctx.clone(), "please provide password as the first argument")?;
return Ok(()) return Ok(());
}; };
match register_user(connect_rac!(ctx), &ctx.name(), pass) { match register_user(connect_rac!(ctx), &ctx.name(), pass) {
Ok(true) => { Ok(true) => {
add_message(ctx.clone(), "you was registered successfully bro")?; add_message(ctx.clone(), "you was registered successfully bro")?;
*ctx.registered.write().unwrap() = Some(pass.to_string()); *ctx.registered.write().unwrap() = Some(pass.to_string());
}, }
Ok(false) => add_message(ctx.clone(), "user with this account already exists bruh")?, Ok(false) => add_message(ctx.clone(), "user with this account already exists bruh")?,
Err(e) => add_message(ctx.clone(), &format!("ERROR while registrationing: {}", e))? Err(e) => add_message(ctx.clone(), &format!("ERROR while registrationing: {}", e))?,
}; };
} else if command == "login" { } else if command == "login" {
let Some(pass) = args.get(0) else { let Some(pass) = args.get(0) else {
add_message(ctx.clone(), "please provide password as the first argument")?; add_message(ctx.clone(), "please provide password as the first argument")?;
return Ok(()) return Ok(());
}; };
add_message(ctx.clone(), "ye bro you was logged in")?; add_message(ctx.clone(), "ye bro you was logged in")?;
*ctx.registered.write().unwrap() = Some(pass.to_string()); *ctx.registered.write().unwrap() = Some(pass.to_string());
} else if command == "ping" { } else if command == "ping" {
let mut before = ctx.packet_size(); let mut before = ctx.packet_size();
let message = format!("Checking ping... {:X}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()); let message = format!(
"Checking ping... {:X}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
);
send_message(connect_rac!(ctx), &message)?; send_message(connect_rac!(ctx), &message)?;
@ -115,11 +123,13 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
loop { loop {
let data = read_messages( let data = read_messages(
connect_rac!(ctx), connect_rac!(ctx),
ctx.config(|o| o.max_messages), ctx.config(|o| o.max_messages),
before, before,
ctx.config(|o| o.chunked_enabled) ctx.config(|o| o.chunked_enabled),
).ok().flatten(); )
.ok()
.flatten();
if let Some((data, size)) = data { if let Some((data, size)) = data {
if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) { if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) {
@ -134,7 +144,10 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
} }
} }
add_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; add_message(
ctx.clone(),
&format!("Ping = {}ms", start.elapsed().unwrap().as_millis()),
)?;
} else { } else {
add_message(ctx.clone(), "Unknown command bruh")?; add_message(ctx.clone(), "Unknown command bruh")?;
} }
@ -143,17 +156,18 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
} }
pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String { pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String {
format!("{}{}{}", format!(
"{}{}{}",
if ctx.config(|o| o.hide_my_ip) { if ctx.config(|o| o.hide_my_ip) {
"\r\x07" "\r\x07"
} else { } else {
"" ""
}, },
message, message,
if !ctx.config(|o| o.hide_my_ip) { if !ctx.config(|o| o.hide_my_ip) {
if message.chars().count() < 54 { if message.chars().count() < 54 {
" ".repeat(54-message.chars().count()) " ".repeat(54 - message.chars().count())
} else { } else {
String::new() String::new()
} }
} else { } else {
@ -172,18 +186,16 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
let last_size = ctx.packet_size(); let last_size = ctx.packet_size();
match read_messages( match read_messages(
connect_rac!(ctx), connect_rac!(ctx),
ctx.config(|o| o.max_messages), ctx.config(|o| o.max_messages),
ctx.packet_size(), ctx.packet_size(),
ctx.config(|o| o.chunked_enabled) ctx.config(|o| o.chunked_enabled),
) { ) {
Ok(Some((messages, size))) => { Ok(Some((messages, size))) => {
if ctx.config(|o| o.chunked_enabled) { if ctx.config(|o| o.chunked_enabled) {
ctx.add_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size); ctx.add_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
if last_size == 0 { if last_size == 0 {
if messages.len() >= 1 { clear_chat_messages(ctx.clone(), messages);
clear_chat_messages(ctx.clone(), messages);
}
} else { } else {
add_chat_messages(ctx.clone(), messages); add_chat_messages(ctx.clone(), messages);
} }
@ -191,10 +203,13 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
ctx.put_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size); ctx.put_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
clear_chat_messages(ctx.clone(), messages); clear_chat_messages(ctx.clone(), messages);
} }
}, }
Err(e) => { Err(e) => {
if ctx.config(|o| o.debug_logs) { if ctx.config(|o| o.debug_logs) {
add_chat_messages(ctx.clone(), vec![format!("Read messages error: {}", e.to_string())]); add_chat_messages(
ctx.clone(),
vec![format!("Read messages error: {}", e.to_string())],
);
} }
} }
_ => {} _ => {}
@ -208,10 +223,10 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
on_command(ctx.clone(), &message)?; on_command(ctx.clone(), &message)?;
} else { } else {
let message = prepare_message( let message = prepare_message(
ctx.clone(), ctx.clone(),
&ctx.config(|o| o.message_format.clone()) &ctx.config(|o| o.message_format.clone())
.replace("{name}", &ctx.name()) .replace("{name}", &ctx.name())
.replace("{text}", &message) .replace("{text}", &message),
); );
if let Some(password) = ctx.registered.read().unwrap().clone() { if let Some(password) = ctx.registered.read().unwrap().clone() {
@ -224,7 +239,7 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
} }
Ok(()) Ok(())
} }
pub fn sanitize_message(message: String) -> Option<String> { pub fn sanitize_message(message: String) -> Option<String> {
let message = sanitize_text(&message); let message = sanitize_text(&message);
@ -235,15 +250,17 @@ pub fn sanitize_message(message: String) -> Option<String> {
} }
/// message -> (date, ip, text, (name, color)) /// message -> (date, ip, text, (name, color))
pub fn parse_message(message: String) -> Option<(String, Option<String>, String, Option<(String, String)>)> { pub fn parse_message(
message: String,
) -> Option<(String, Option<String>, String, Option<(String, String)>)> {
if message.is_empty() { if message.is_empty() {
return None return None;
} }
let date = DATE_REGEX.captures(&message)?; let date = DATE_REGEX.captures(&message)?;
let (date, message) = ( let (date, message) = (
date.get(1)?.as_str().to_string(), date.get(1)?.as_str().to_string(),
date.get(2)?.as_str().to_string(), date.get(2)?.as_str().to_string(),
); );
let message = message let message = message
@ -254,11 +271,14 @@ pub fn parse_message(message: String) -> Option<(String, Option<String>, String,
.to_string(); .to_string();
let (ip, message) = if let Some(message) = IP_REGEX.captures(&message) { let (ip, message) = if let Some(message) = IP_REGEX.captures(&message) {
(Some(message.get(1)?.as_str().to_string()), message.get(2)?.as_str().to_string()) (
Some(message.get(1)?.as_str().to_string()),
message.get(2)?.as_str().to_string(),
)
} else { } else {
(None, message) (None, message)
}; };
let (message, nick) = match find_username_color(&message) { let (message, nick) = match find_username_color(&message) {
Some((name, content, color)) => (content, Some((name, color))), Some((name, content, color)) => (content, Some((name, color))),
None => (message, None), None => (message, None),
@ -271,7 +291,11 @@ pub fn parse_message(message: String) -> Option<(String, Option<String>, String,
pub fn find_username_color(message: &str) -> Option<(String, String, String)> { pub fn find_username_color(message: &str) -> Option<(String, String, String)> {
for (re, color) in COLORED_USERNAMES.iter() { for (re, color) in COLORED_USERNAMES.iter() {
if let Some(captures) = re.captures(message) { if let Some(captures) = re.captures(message) {
return Some((captures[1].to_string(), captures[2].to_string(), color.clone())) return Some((
captures[1].to_string(),
captures[2].to_string(),
color.clone(),
));
} }
} }
None None

View File

@ -1,4 +1,4 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub mod chat; pub mod chat;
pub mod proto; pub mod proto;

View File

@ -1,15 +1,21 @@
use std::sync::Arc; use std::sync::Arc;
use bRAC::chat::{
config::{get_config_path, load_config, Args},
ctx::Context,
run_main_loop,
};
use bRAC::proto::{connect, read_messages, send_message}; use bRAC::proto::{connect, read_messages, send_message};
use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop}; use clap::Parser;
use clap::Parser;
fn main() { fn main() {
#[cfg(feature = "winapi")] #[cfg(feature = "winapi")]
unsafe { winapi::um::wincon::FreeConsole() }; unsafe {
winapi::um::wincon::FreeConsole()
};
let args = Args::parse(); let args = Args::parse();
let config_path = get_config_path(); let config_path = get_config_path();
if args.config_path { if args.config_path {
@ -19,35 +25,34 @@ fn main() {
let mut config = load_config(config_path); let mut config = load_config(config_path);
if args.read_messages { args.patch_config(&mut config);
let mut stream = connect(&config.host, config.proxy.clone()).expect("Error reading message");
print!("{}", read_messages( if args.read_messages {
&mut stream, let mut stream =
config.max_messages, connect(&config.host, config.proxy.clone()).expect("Error reading message");
0,
false print!(
) "{}",
.ok().flatten() read_messages(&mut stream, config.max_messages, 0, false)
.expect("Error reading messages").0.join("\n") .ok()
.flatten()
.expect("Error reading messages")
.0
.join("\n")
); );
} }
if let Some(message) = &args.send_message { if let Some(message) = &args.send_message {
let mut stream = connect(&config.host, config.proxy.clone()).expect("Error sending message"); let mut stream =
connect(&config.host, config.proxy.clone()).expect("Error sending message");
send_message( send_message(&mut stream, message).expect("Error sending message");
&mut stream,
message
).expect("Error sending message");
} }
if args.send_message.is_some() || args.read_messages { if args.send_message.is_some() || args.read_messages {
return; return;
} }
args.patch_config(&mut config);
let ctx = Arc::new(Context::new(&config)); let ctx = Arc::new(Context::new(&config));
run_main_loop(ctx.clone()); run_main_loop(ctx.clone());

View File

@ -1,4 +1,10 @@
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, time::Duration}; use std::{
error::Error,
fmt::Debug,
io::{Read, Write},
net::{TcpStream, ToSocketAddrs},
time::Duration,
};
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use socks::Socks5Stream; use socks::Socks5Stream;
@ -13,28 +19,44 @@ pub trait Stream: Read + Write + Unpin + Send + Sync + Debug {
} }
impl Stream for TcpStream { impl Stream for TcpStream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); } let _ = TcpStream::set_read_timeout(&self, Some(timeout));
}
fn set_write_timeout(&self, timeout: Duration) {
let _ = TcpStream::set_write_timeout(&self, Some(timeout));
}
} }
impl Stream for Socks5Stream { impl Stream for Socks5Stream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout)); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout)); } let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout));
}
fn set_write_timeout(&self, timeout: Duration) {
let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout));
}
} }
impl<T: Stream> Stream for TlsStream<T> { impl<T: Stream> Stream for TlsStream<T> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } self.get_ref().set_read_timeout(timeout);
}
fn set_write_timeout(&self, timeout: Duration) {
self.get_ref().set_write_timeout(timeout);
}
} }
impl Stream for TlsStream<Box<dyn Stream>> { impl Stream for TlsStream<Box<dyn Stream>> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } self.get_ref().set_read_timeout(timeout);
}
fn set_write_timeout(&self, timeout: Duration) {
self.get_ref().set_write_timeout(timeout);
}
} }
pub enum RacStream { pub enum RacStream {
WRAC(WebSocket<Box<dyn Stream>>), WRAC(WebSocket<Box<dyn Stream>>),
RAC(Box<dyn Stream>) RAC(Box<dyn Stream>),
} }
/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \ /// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \
@ -64,46 +86,42 @@ pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
let (scheme, url) = url.split_once("://").unwrap_or(("rac", url)); let (scheme, url) = url.split_once("://").unwrap_or(("rac", url));
let (host, _) = url.split_once("/").unwrap_or((url, "")); let (host, _) = url.split_once("/").unwrap_or((url, ""));
match scheme.to_lowercase().as_str() { match scheme.to_lowercase().as_str() {
"rac" => { "rac" => Some((
Some(( if host.contains(":") {
if host.contains(":") { host.to_string()
host.to_string() } else {
} else { format!("{host}:42666")
format!("{host}:42666") },
}, false,
false, false false,
)) )),
}, "racs" => Some((
"racs" => { if host.contains(":") {
Some(( host.to_string()
if host.contains(":") { } else {
host.to_string() format!("{host}:42667")
} else { },
format!("{host}:42667") true,
}, false,
true, false )),
)) "wrac" => Some((
}, if host.contains(":") {
"wrac" => { host.to_string()
Some(( } else {
if host.contains(":") { format!("{host}:52666")
host.to_string() },
} else { false,
format!("{host}:52666") true,
}, )),
false, true "wracs" => Some((
)) if host.contains(":") {
}, host.to_string()
"wracs" => { } else {
Some(( format!("{host}:52667")
if host.contains(":") { },
host.to_string() true,
} else { true,
format!("{host}:52667") )),
},
true, true
))
},
_ => None, _ => None,
} }
} }
@ -115,12 +133,18 @@ pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
/// proxy - socks5 proxy (host, (user, pass)) /// proxy - socks5 proxy (host, (user, pass))
/// wrac - to use wrac protocol /// wrac - to use wrac protocol
pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn Error>> { pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn Error>> {
let (host, ssl, wrac) = parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?; let (host, ssl, wrac) =
parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?;
let stream: Box<dyn Stream> = if let Some(proxy) = proxy { let stream: Box<dyn Stream> = if let Some(proxy) = proxy {
if let Some((proxy, auth)) = parse_socks5_url(&proxy) { if let Some((proxy, auth)) = parse_socks5_url(&proxy) {
if let Some((user, pass)) = auth { if let Some((user, pass)) = auth {
Box::new(Socks5Stream::connect_with_password(&proxy, host.as_str(), &user, &pass)?) Box::new(Socks5Stream::connect_with_password(
&proxy,
host.as_str(),
&user,
&pass,
)?)
} else { } else {
Box::new(Socks5Stream::connect(&proxy, host.as_str())?) Box::new(Socks5Stream::connect(&proxy, host.as_str())?)
} }
@ -128,21 +152,27 @@ pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn E
return Err("proxy parse error".into()); return Err("proxy parse error".into());
} }
} else { } else {
let addr = host.to_socket_addrs()?.next().ok_or::<Box<dyn Error>>("addr parse error".into())?; let addr = host
.to_socket_addrs()?
.next()
.ok_or::<Box<dyn Error>>("addr parse error".into())?;
Box::new(TcpStream::connect(&addr)?) Box::new(TcpStream::connect(&addr)?)
}; };
let stream = if ssl { let stream = if ssl {
let ip: String = host.split_once(":") let ip: String = host
.split_once(":")
.map(|o| o.0.to_string()) .map(|o| o.0.to_string())
.unwrap_or(host.clone()); .unwrap_or(host.clone());
Box::new(TlsConnector::builder() Box::new(
.danger_accept_invalid_certs(true) TlsConnector::builder()
.danger_accept_invalid_hostnames(true) .danger_accept_invalid_certs(true)
.build()? .danger_accept_invalid_hostnames(true)
.connect(&ip, stream)?) .build()?
.connect(&ip, stream)?,
)
} else { } else {
stream stream
}; };
@ -152,8 +182,8 @@ pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn E
if wrac { if wrac {
let (client, _) = tungstenite::client( let (client, _) = tungstenite::client(
&format!("ws{}://{host}", if ssl { "s" } else { "" }), &format!("ws{}://{host}", if ssl { "s" } else { "" }),
stream stream,
)?; )?;
Ok(RacStream::WRAC(client)) Ok(RacStream::WRAC(client))
} else { } else {
@ -171,8 +201,13 @@ pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn E
/// register_user(stream, name, name) /// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message) /// send_message_spoof_auth(stream, name + "> " + message)
/// } /// }
pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str) -> Result<(), Box<dyn Error>> { pub fn send_message_spoof_auth(
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) }; stream: &mut RacStream,
message: &str,
) -> Result<(), Box<dyn Error>> {
let Some((name, message)) = message.split_once("> ") else {
return send_message(stream, message);
};
if let Ok(f) = send_message_auth(stream, &name, &name, &message) { if let Ok(f) = send_message_auth(stream, &name, &name, &message) {
if f != 0 { if f != 0 {
@ -185,18 +220,14 @@ pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str) -> Result<
Ok(()) Ok(())
} }
/// Send message /// Send message
/// ///
/// stream - any stream that can be written to /// stream - any stream that can be written to
/// message - message text /// message - message text
pub fn send_message( pub fn send_message(stream: &mut RacStream, message: &str) -> Result<(), Box<dyn Error>> {
stream: &mut RacStream,
message: &str
) -> Result<(), Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::send_message(websocket, message), RacStream::WRAC(websocket) => wrac::send_message(websocket, message),
RacStream::RAC(stream) => rac::send_message(stream, message) RacStream::RAC(stream) => rac::send_message(stream, message),
} }
} }
@ -209,13 +240,13 @@ pub fn send_message(
/// ///
/// returns whether the user was registered /// returns whether the user was registered
pub fn register_user( pub fn register_user(
stream: &mut RacStream, stream: &mut RacStream,
name: &str, name: &str,
password: &str password: &str,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password), RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password),
RacStream::RAC(stream) => rac::register_user(stream, name, password) RacStream::RAC(stream) => rac::register_user(stream, name, password),
} }
} }
@ -231,14 +262,14 @@ pub fn register_user(
/// returns 1 if the user does not exist /// returns 1 if the user does not exist
/// returns 2 if the password is incorrect /// returns 2 if the password is incorrect
pub fn send_message_auth( pub fn send_message_auth(
stream: &mut RacStream, stream: &mut RacStream,
name: &str, name: &str,
password: &str, password: &str,
message: &str message: &str,
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message), RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message),
RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message) RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message),
} }
} }
@ -251,13 +282,15 @@ pub fn send_message_auth(
/// ///
/// returns (messages, packet size) /// returns (messages, packet size)
pub fn read_messages( pub fn read_messages(
stream: &mut RacStream, stream: &mut RacStream,
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
chunked: bool chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::read_messages(websocket, max_messages, last_size, chunked), RacStream::WRAC(websocket) => {
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, chunked) wrac::read_messages(websocket, max_messages, last_size, chunked)
}
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, chunked),
} }
} }

View File

@ -1,4 +1,7 @@
use std::{error::Error, io::{Read, Write}}; use std::{
error::Error,
io::{Read, Write},
};
/// Send message /// Send message
/// ///
@ -18,9 +21,9 @@ pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box<dy
/// ///
/// returns whether the user was registered /// returns whether the user was registered
pub fn register_user( pub fn register_user(
stream: &mut (impl Write + Read), stream: &mut (impl Write + Read),
name: &str, name: &str,
password: &str password: &str,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
if let Ok(out) = skip_null(stream) { if let Ok(out) = skip_null(stream) {
@ -42,9 +45,9 @@ pub fn register_user(
/// returns 1 if the user does not exist /// returns 1 if the user does not exist
/// returns 2 if the password is incorrect /// returns 2 if the password is incorrect
pub fn send_message_auth( pub fn send_message_auth(
stream: &mut (impl Write + Read), stream: &mut (impl Write + Read),
name: &str, name: &str,
password: &str, password: &str,
message: &str, message: &str,
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?; stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?;
@ -61,7 +64,7 @@ pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
let mut buf = vec![0; 1]; let mut buf = vec![0; 1];
stream.read_exact(&mut buf)?; stream.read_exact(&mut buf)?;
if buf[0] != 0 { if buf[0] != 0 {
break Ok(buf) break Ok(buf);
} }
} }
} }
@ -69,7 +72,7 @@ pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
/// remove trailing null bytes in vector /// remove trailing null bytes in vector
pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> { pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
while vec.ends_with(&[0]) { while vec.ends_with(&[0]) {
vec.remove(vec.len()-1); vec.remove(vec.len() - 1);
} }
Ok(()) Ok(())
} }
@ -83,10 +86,10 @@ pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
/// ///
/// returns (messages, packet size) /// returns (messages, packet size)
pub fn read_messages( pub fn read_messages(
stream: &mut (impl Read + Write), stream: &mut (impl Read + Write),
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
chunked: bool chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write_all(&[0x00])?; stream.write_all(&[0x00])?;
@ -123,10 +126,16 @@ pub fn read_messages(
let packet_data = String::from_utf8_lossy(&packet_data).to_string(); let packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect(); let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter() let lines: Vec<String> = lines
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) .clone()
.into_iter()
.skip(if lines.len() >= max_messages {
lines.len() - max_messages
} else {
0
})
.map(|o| o.to_string()) .map(|o| o.to_string())
.collect(); .collect();
Ok(Some((lines, packet_size))) Ok(Some((lines, packet_size)))
} }

View File

@ -1,6 +1,8 @@
use std::{error::Error, io::{Read, Write}}; use std::{
use tungstenite::{WebSocket, Message}; error::Error,
io::{Read, Write},
};
use tungstenite::{Message, WebSocket};
/// Send message /// Send message
/// ///
@ -8,9 +10,11 @@ use tungstenite::{WebSocket, Message};
/// message - message text /// message - message text
pub fn send_message( pub fn send_message(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
message: &str message: &str,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
stream.write(Message::Binary(format!("\x01{message}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x01{message}").as_bytes().to_vec().into(),
))?;
stream.flush()?; stream.flush()?;
Ok(()) Ok(())
} }
@ -23,11 +27,13 @@ pub fn send_message(
/// ///
/// returns whether the user was registered /// returns whether the user was registered
pub fn register_user( pub fn register_user(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
name: &str, name: &str,
password: &str password: &str,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x03{name}\n{password}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x03{name}\n{password}").as_bytes().to_vec().into(),
))?;
stream.flush()?; stream.flush()?;
if let Ok(msg) = stream.read() { if let Ok(msg) = stream.read() {
Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0) Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0)
@ -47,12 +53,17 @@ pub fn register_user(
/// returns 1 if the user does not exist /// returns 1 if the user does not exist
/// returns 2 if the password is incorrect /// returns 2 if the password is incorrect
pub fn send_message_auth( pub fn send_message_auth(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
name: &str, name: &str,
password: &str, password: &str,
message: &str message: &str,
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x02{name}\n{password}\n{message}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x02{name}\n{password}\n{message}")
.as_bytes()
.to_vec()
.into(),
))?;
stream.flush()?; stream.flush()?;
if let Ok(msg) = stream.read() { if let Ok(msg) = stream.read() {
if msg.is_binary() { if msg.is_binary() {
@ -73,10 +84,10 @@ pub fn send_message_auth(
/// ///
/// returns (messages, packet size) /// returns (messages, packet size)
pub fn read_messages( pub fn read_messages(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
chunked: bool chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write(Message::Binary(vec![0x00].into()))?; stream.write(Message::Binary(vec![0x00].into()))?;
stream.flush()?; stream.flush()?;
@ -101,7 +112,9 @@ pub fn read_messages(
stream.write(Message::Binary(vec![0x00, 0x01].into()))?; stream.write(Message::Binary(vec![0x00, 0x01].into()))?;
packet_size packet_size
} else { } else {
stream.write(Message::Binary(format!("\x00\x02{}", last_size).as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x00\x02{}", last_size).as_bytes().to_vec().into(),
))?;
packet_size - last_size packet_size - last_size
}; };
stream.flush()?; stream.flush()?;
@ -119,10 +132,16 @@ pub fn read_messages(
let packet_data = String::from_utf8_lossy(&packet_data).to_string(); let packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect(); let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter() let lines: Vec<String> = lines
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) .clone()
.into_iter()
.skip(if lines.len() >= max_messages {
lines.len() - max_messages
} else {
0
})
.map(|o| o.to_string()) .map(|o| o.to_string())
.collect(); .collect();
Ok(Some((lines, packet_size))) Ok(Some((lines, packet_size)))
} }