This commit is contained in:
MeexReay 2025-06-17 00:03:25 +03:00
parent 3e75662969
commit c1e9d00d3a
10 changed files with 820 additions and 502 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

@ -1,38 +1,69 @@
use clap::Parser;
use serde_default::DefaultFromSerde;
use serde_yml;
use std::str::FromStr;
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}";
fn default_true() -> bool { true }
pub fn default_max_messages() -> usize { 200 }
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() }
fn default_true() -> bool {
true
}
pub fn default_max_messages() -> usize {
200
}
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)]
pub struct Config {
#[serde(default = "default_host")] pub host: String,
#[serde(default)] pub name: Option<String>,
#[serde(default = "default_message_format")] pub message_format: String,
#[serde(default = "default_update_time")] pub update_time: usize,
#[serde(default = "default_oof_update_time")] pub oof_update_time: usize,
#[serde(default = "default_max_messages")] pub max_messages: usize,
#[serde(default = "default_konata_size")] pub konata_size: usize,
#[serde(default)] pub remove_gui_shit: 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,
#[serde(default = "default_host")]
pub host: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default = "default_message_format")]
pub message_format: String,
#[serde(default = "default_update_time")]
pub update_time: usize,
#[serde(default = "default_oof_update_time")]
pub oof_update_time: usize,
#[serde(default = "default_max_messages")]
pub max_messages: usize,
#[serde(default = "default_konata_size")]
pub konata_size: usize,
#[serde(default)]
pub remove_gui_shit: 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 {
@ -98,54 +129,105 @@ pub fn save_config(path: PathBuf, config: &Config) {
#[command(version, about, long_about = None)]
pub struct Args {
/// Print config path
#[arg(short='p', long)]
#[arg(short = 'p', long)]
pub config_path: bool,
/// Print unformatted messages from chat and exit
#[arg(short='r', long)]
#[arg(short = 'r', long)]
pub read_messages: bool,
/// 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>,
#[arg(short='H', long)] pub host: Option<String>,
#[arg(short='n', long)] pub name: Option<String>,
#[arg(long)] pub message_format: Option<String>,
#[arg(long)] pub update_time: Option<usize>,
#[arg(long)] pub oof_update_time: Option<usize>,
#[arg(long)] pub max_messages: Option<usize>,
#[arg(long)] pub konata_size: Option<usize>,
#[arg(long)] pub hide_my_ip: Option<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,
#[arg(short = 'H', long)]
pub host: Option<String>,
#[arg(short = 'n', long)]
pub name: Option<String>,
#[arg(long)]
pub message_format: Option<String>,
#[arg(long)]
pub update_time: Option<usize>,
#[arg(long)]
pub oof_update_time: Option<usize>,
#[arg(long)]
pub max_messages: Option<usize>,
#[arg(long)]
pub konata_size: Option<usize>,
#[arg(long)]
pub hide_my_ip: Option<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 {
pub fn patch_config(&self, config: &mut Config) {
if let Some(v) = self.host.clone() { config.host = v }
if let Some(v) = self.name.clone() { config.name = Some(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.update_time { config.update_time = 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.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 }
if let Some(v) = self.host.clone() {
config.host = v
}
if let Some(v) = self.name.clone() {
config.name = Some(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.update_time {
config.update_time = 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.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;
@ -11,7 +15,7 @@ pub struct Context {
pub messages: RwLock<Vec<String>>,
pub packet_size: AtomicUsize,
pub name: RwLock<String>,
pub is_focused: AtomicBool
pub is_focused: AtomicBool,
}
impl Context {
@ -22,8 +26,13 @@ impl Context {
sender: RwLock::new(None),
messages: RwLock::new(Vec::new()),
packet_size: AtomicUsize::default(),
name: RwLock::new(config.name.clone().unwrap_or_else(|| format!("Anon#{:X}", random::<u16>()))),
is_focused: AtomicBool::new(true)
name: RwLock::new(
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) {
*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.messages.write().unwrap() = Vec::new();
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())
}
@ -51,7 +63,12 @@ impl Context {
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);
let mut messages = messages;
if messages.len() > max_length {
@ -60,7 +77,12 @@ impl Context {
*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.add_message(max_length, messages);
}
@ -75,10 +97,10 @@ impl Context {
#[macro_export]
macro_rules! connect_rac {
($ctx:ident) => {
($ctx:ident) => {
&mut connect(
&$ctx.config(|o| o.host.clone()),
$ctx.config(|o| o.proxy.clone())
)?
&$ctx.config(|o| o.host.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::time::{Duration, SystemTime};
use std::thread;
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 gtk4::{self as gtk};
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::prelude::*;
use gtk::gdk::{Cursor, Display, Texture};
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu};
use gtk::glib::clone;
use gtk::glib::{
self, clone::Downgrade,
timeout_add_local,
source::timeout_add_local_once,
self, clone::Downgrade, source::timeout_add_local_once, timeout_add_local, timeout_add_once,
ControlFlow,
timeout_add_once
};
use gtk::pango::WrapMode;
use gtk::prelude::*;
use gtk::{
AboutDialog, Align, Application, ApplicationWindow, Box as GtkBox,
Button, Calendar, CheckButton, CssProvider, Entry, Fixed, GestureClick,
Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings, Window
AboutDialog, Align, Application, ApplicationWindow, Box as GtkBox, Button, Calendar,
CheckButton, CssProvider, Entry, Fixed, GestureClick, Justification, Label, ListBox,
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},
ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize_message, SERVER_LIST};
use super::{
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 {
is_dark_theme: bool,
@ -39,7 +42,7 @@ struct UiModel {
#[cfg(feature = "libnotify")]
notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
#[cfg(not(feature = "libnotify"))]
notifications: Arc<RwLock<Vec<String>>>
notifications: Arc<RwLock<Vec<String>>>,
}
thread_local!(
@ -47,11 +50,23 @@ thread_local!(
);
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>) {
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>> {
@ -62,97 +77,83 @@ fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> {
}
macro_rules! gui_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
hbox.append(&Label::builder().label($e).build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()))
.build();
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()))
.build();
hbox.append(&entry);
hbox.append(&entry);
$vbox.append(&hbox);
$vbox.append(&hbox);
entry
}
};
entry
}};
}
macro_rules! gui_usize_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
hbox.append(&Label::builder().label($e).build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.to_string()))
.build();
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.to_string()))
.build();
hbox.append(&entry);
hbox.append(&entry);
$vbox.append(&hbox);
$vbox.append(&hbox);
entry
}
};
entry
}};
}
macro_rules! gui_option_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
hbox.append(&Label::builder().label($e).build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default())
.build();
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default())
.build();
hbox.append(&entry);
hbox.append(&entry);
$vbox.append(&hbox);
$vbox.append(&hbox);
entry
}
};
entry
}};
}
macro_rules! gui_checkbox_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
hbox.append(&Label::builder().label($e).build());
let entry = CheckButton::builder()
.active($ctx.config(|o| o.$i))
.build();
let entry = CheckButton::builder().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>) {
GLOBAL.with(|global| {
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 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 update_time_entry = gui_usize_entry_setting!("Update Time", update_time, 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 update_time_entry =
gui_usize_entry_setting!("Update Time", update_time, 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 show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, settings_vbox);
let auth_enabled_entry = gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, settings_vbox);
let chunked_enabled_entry = 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 show_other_ip_entry =
gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, settings_vbox);
let auth_enabled_entry =
gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, settings_vbox);
let chunked_enabled_entry =
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 konata_size_entry = 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 konata_size_entry =
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()
.child(&settings_vbox)
@ -193,37 +214,53 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
vbox.append(&scrollable);
let save_button = Button::builder()
.label("Save")
.build();
let save_button = Button::builder().label("Save").build();
vbox.append(&save_button);
save_button.connect_clicked(clone!(
#[weak] ctx,
#[weak] host_entry,
#[weak] name_entry,
#[weak] message_format_entry,
#[weak] update_time_entry,
#[weak] max_messages_entry,
#[weak] 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,
#[weak] debug_logs_entry,
#[weak] oof_update_time_entry,
#[weak] konata_size_entry,
#[weak] remove_gui_shit_entry,
#[weak]
ctx,
#[weak]
host_entry,
#[weak]
name_entry,
#[weak]
message_format_entry,
#[weak]
update_time_entry,
#[weak]
max_messages_entry,
#[weak]
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,
#[weak]
debug_logs_entry,
#[weak]
oof_update_time_entry,
#[weak]
konata_size_entry,
#[weak]
remove_gui_shit_entry,
move |_| {
let config = Config {
host: host_entry.text().to_string(),
name: {
let name = name_entry.text().to_string();
if name.is_empty() {
None
} else {
@ -233,7 +270,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
message_format: message_format_entry.text().to_string(),
update_time: {
let update_time = update_time_entry.text();
if let Ok(update_time) = update_time.parse::<usize>() {
update_time
} else {
@ -244,7 +281,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
},
oof_update_time: {
let oof_update_time = oof_update_time_entry.text();
if let Ok(oof_update_time) = oof_update_time.parse::<usize>() {
oof_update_time
} else {
@ -255,7 +292,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
},
konata_size: {
let konata_size = konata_size_entry.text();
if let Ok(konata_size) = konata_size.parse::<usize>() {
konata_size.max(0).min(200)
} else {
@ -266,7 +303,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
},
max_messages: {
let max_messages = max_messages_entry.text();
if let Ok(max_messages) = max_messages.parse::<usize>() {
max_messages
} else {
@ -286,13 +323,13 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
debug_logs: debug_logs_entry.is_active(),
proxy: {
let proxy = proxy_entry.text().to_string();
if proxy.is_empty() {
None
} else {
Some(proxy)
}
}
},
};
ctx.set_config(&config);
save_config(get_config_path(), &config);
@ -300,27 +337,39 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
}
));
let reset_button = Button::builder()
.label("Reset all")
.build();
let reset_button = Button::builder().label("Reset all").build();
vbox.append(&reset_button);
reset_button.connect_clicked(clone!(
#[weak] ctx,
#[weak] host_entry,
#[weak] name_entry,
#[weak] message_format_entry,
#[weak] update_time_entry,
#[weak] max_messages_entry,
#[weak] 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,
#[weak]
ctx,
#[weak]
host_entry,
#[weak]
name_entry,
#[weak]
message_format_entry,
#[weak]
update_time_entry,
#[weak]
max_messages_entry,
#[weak]
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 |_| {
let config = Config::default();
ctx.set_config(&config);
@ -365,7 +414,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
});
window.add_controller(controller);
window.present();
}
@ -387,7 +436,8 @@ fn build_menu(ctx: Arc<Context>, app: &Application) {
app.add_action_entries([
ActionEntry::builder("settings")
.activate(clone!(
#[weak] ctx,
#[weak]
ctx,
move |a: &Application, _, _| {
open_settings(ctx, a);
}
@ -400,12 +450,14 @@ fn build_menu(ctx: Arc<Context>, app: &Application) {
.build(),
ActionEntry::builder("about")
.activate(clone!(
#[weak] app,
#[weak]
app,
move |_, _, _| {
AboutDialog::builder()
AboutDialog::builder()
.application(&app)
.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
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
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")
.website("https://github.com/MeexReay/bRAC")
.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()
.present();
}
))
.build()
.build(),
]);
}
fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let is_dark_theme = if let Some(settings) = Settings::default() {
settings.is_gtk_application_prefer_dark_theme() || settings.gtk_theme_name()
.map(|o| o.to_lowercase().contains("dark"))
.unwrap_or_default()
settings.is_gtk_application_prefer_dark_theme()
|| settings
.gtk_theme_name()
.map(|o| o.to_lowercase().contains("dark"))
.unwrap_or_default()
} else {
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);
if !remove_gui_shit {
widget_box.append(&Calendar::builder()
.css_classes(["calendar"])
.show_heading(false)
.can_target(false)
.build());
widget_box.append(
&Calendar::builder()
.css_classes(["calendar"])
.show_heading(false)
.can_target(false)
.build(),
);
}
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() {
let url = url.to_string();
let label = Label::builder()
.label(&url)
.halign(Align::Start)
.build();
let label = Label::builder().label(&url).halign(Align::Start).build();
let click = GestureClick::new();
click.connect_pressed(clone!(
#[weak] ctx,
#[weak]
ctx,
move |_, _, _, _| {
let mut config = ctx.config.read().unwrap().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 = 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);
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");
@ -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);
let logo_anim = PixbufAnimation::from_stream(
&MemoryInputStream::from_bytes(
&glib::Bytes::from(logo_gif)
),
None::<&gio::Cancellable>
).unwrap().iter(Some(SystemTime::now()));
&MemoryInputStream::from_bytes(&glib::Bytes::from(logo_gif)),
None::<&gio::Cancellable>,
)
.unwrap()
.iter(Some(SystemTime::now()));
timeout_add_local(Duration::from_millis(30), {
let logo = logo.clone();
@ -531,9 +593,13 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
ControlFlow::Continue
}
});
// 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()
.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);
widget_box_overlay.add_overlay(&fixed);
}
widget_box_overlay.set_child(Some(&widget_box));
@ -599,16 +664,24 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
.build();
send_btn.connect_clicked(clone!(
#[weak] text_entry,
#[weak] ctx,
#[weak]
text_entry,
#[weak]
ctx,
move |_| {
if text_entry.text().is_empty() { return; }
timeout_add_local_once(Duration::ZERO, clone!(
#[weak] text_entry,
move || {
text_entry.set_text("");
}
));
if text_entry.text().is_empty() {
return;
}
timeout_add_local_once(
Duration::ZERO,
clone!(
#[weak]
text_entry,
move || {
text_entry.set_text("");
}
),
);
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
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!(
#[weak] text_entry,
#[weak] ctx,
#[weak]
text_entry,
#[weak]
ctx,
move |_| {
if text_entry.text().is_empty() { return; }
timeout_add_local_once(Duration::ZERO, clone!(
#[weak] text_entry,
move || {
text_entry.set_text("");
}
));
if text_entry.text().is_empty() {
return;
}
timeout_add_local_once(
Duration::ZERO,
clone!(
#[weak]
text_entry,
move || {
text_entry.set_text("");
}
),
);
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
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, {
let scrolled_window_weak = scrolled_window_weak.clone();
move || {
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()
.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_height(500)
.resizable(true)
@ -674,7 +760,8 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let scrolled_window_weak = scrolled_window_weak.clone();
timeout_add_local_once(Duration::ZERO, move || {
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));
run_recv_loop(ctx.clone());
ui.window.connect_notify(Some("is-active"), {
let ctx = ctx.clone();
@ -765,7 +852,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
fn load_css(is_dark_theme: bool) {
let provider = CssProvider::new();
provider.load_from_data(&format!(
"{}\n{}",
"{}\n{}",
if is_dark_theme {
include_str!("styles/dark.css")
} else {
@ -788,7 +875,9 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
let notification = Notification::new(title, message, None);
notification.set_app_name("bRAC");
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();
notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap());
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"))]
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;
@ -806,7 +898,14 @@ fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str)
hash.write(title.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);
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) {
let Some(message) = sanitize_message(message) else { return; };
let Some(message) = sanitize_message(message) else {
return;
};
if message.is_empty() {
return;
@ -825,17 +926,9 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
// TODO: softcode these colors
let (ip_color, date_color, text_color) = if ui.is_dark_theme {
(
"#494949",
"#929292",
"#FFFFFF"
)
("#494949", "#929292", "#FFFFFF")
} else {
(
"#585858",
"#292929",
"#000000"
)
("#585858", "#292929", "#000000")
};
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(ip) = 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 {
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 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 {
@ -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 {
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 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);
hbox.append(&Label::builder()
.label(&label)
.halign(Align::Start)
.valign(Align::Start)
.selectable(true)
.wrap(true)
.wrap_mode(WrapMode::WordChar)
.use_markup(true)
.build());
hbox.append(
&Label::builder()
.label(&label)
.halign(Align::Start)
.valign(Align::Start)
.selectable(true)
.wrap(true)
.wrap_mode(WrapMode::WordChar)
.use_markup(true)
.build(),
);
hbox.set_hexpand(true);
@ -896,7 +1012,8 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
GLOBAL.with(|global| {
if let Some(ui) = &*global.borrow() {
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>) {
if let Err(e) = recv_tick(ctx.clone()) {
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));
}
}
fn run_recv_loop(ctx: Arc<Context>) {
thread::spawn(move || {
loop {
make_recv_tick(ctx.clone());
thread::spawn(move || loop {
make_recv_tick(ctx.clone());
thread::sleep(Duration::from_millis(
if ctx.is_focused.load(Ordering::SeqCst) {
ctx.config(|o| o.update_time) as u64
} else {
ctx.config(|o| o.oof_update_time) as u64
}
));
}
thread::sleep(Duration::from_millis(
if ctx.is_focused.load(Ordering::SeqCst) {
ctx.config(|o| o.update_time) as u64
} else {
ctx.config(|o| o.oof_update_time) as u64
},
));
});
}

View File

@ -1,10 +1,14 @@
use std::{
error::Error, sync::Arc, time::{SystemTime, UNIX_EPOCH}
error::Error,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
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 lazy_static::lazy_static;
@ -14,7 +18,6 @@ use ctx::Context;
pub use gui::run_main_loop;
const HELP_MESSAGE: &str = "Help message:
/help - show help message
/register password - register user
@ -38,16 +41,15 @@ lazy_static! {
];
pub static ref SERVER_LIST: Vec<String> = vec![
"rac://meex.lol".to_string(),
"rac://meex.lol:11234".to_string(),
"rac://meex.lol".to_string(),
"rac://meex.lol:11234".to_string(),
"rac://91.192.22.20".to_string()
];
}
pub mod gui;
pub mod config;
pub mod ctx;
pub mod gui;
pub fn sanitize_text(input: &str) -> String {
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>> {
for i in message.split("\n")
.map(|o| o.to_string()) {
for i in message.split("\n").map(|o| o.to_string()) {
print_message(ctx.clone(), i)?;
}
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>>();
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()?;
for _ in 0..times {
send_message(connect_rac!(ctx), "\r")?;
}
} 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 msg = args[1..].join(" ");
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" {
add_message(ctx.clone(), HELP_MESSAGE)?;
} 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")?;
return Ok(())
return Ok(());
};
match register_user(connect_rac!(ctx), &ctx.name(), pass) {
Ok(true) => {
add_message(ctx.clone(), "you was registered successfully bro")?;
*ctx.registered.write().unwrap() = Some(pass.to_string());
},
}
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" {
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")?;
return Ok(())
return Ok(());
};
add_message(ctx.clone(), "ye bro you was logged in")?;
*ctx.registered.write().unwrap() = Some(pass.to_string());
} else if command == "ping" {
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)?;
@ -115,11 +123,13 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
loop {
let data = read_messages(
connect_rac!(ctx),
ctx.config(|o| o.max_messages),
before,
ctx.config(|o| o.chunked_enabled)
).ok().flatten();
connect_rac!(ctx),
ctx.config(|o| o.max_messages),
before,
ctx.config(|o| o.chunked_enabled),
)
.ok()
.flatten();
if let Some((data, size)) = data {
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 {
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 {
format!("{}{}{}",
format!(
"{}{}{}",
if ctx.config(|o| o.hide_my_ip) {
"\r\x07"
} else {
""
},
message,
if !ctx.config(|o| o.hide_my_ip) {
if message.chars().count() < 54 {
" ".repeat(54-message.chars().count())
} else {
if !ctx.config(|o| o.hide_my_ip) {
if message.chars().count() < 54 {
" ".repeat(54 - message.chars().count())
} else {
String::new()
}
} else {
@ -172,18 +186,16 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
let last_size = ctx.packet_size();
match read_messages(
connect_rac!(ctx),
ctx.config(|o| o.max_messages),
ctx.packet_size(),
ctx.config(|o| o.chunked_enabled)
connect_rac!(ctx),
ctx.config(|o| o.max_messages),
ctx.packet_size(),
ctx.config(|o| o.chunked_enabled),
) {
Ok(Some((messages, size))) => {
if ctx.config(|o| o.chunked_enabled) {
ctx.add_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
if last_size == 0 {
if messages.len() >= 1 {
clear_chat_messages(ctx.clone(), messages);
}
clear_chat_messages(ctx.clone(), messages);
} else {
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);
clear_chat_messages(ctx.clone(), messages);
}
},
}
Err(e) => {
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)?;
} else {
let message = prepare_message(
ctx.clone(),
&ctx.config(|o| o.message_format.clone())
.replace("{name}", &ctx.name())
.replace("{text}", &message)
ctx.clone(),
&ctx.config(|o| o.message_format.clone())
.replace("{name}", &ctx.name())
.replace("{text}", &message),
);
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(())
}
}
pub fn sanitize_message(message: String) -> Option<String> {
let message = sanitize_text(&message);
@ -235,15 +250,17 @@ pub fn sanitize_message(message: String) -> Option<String> {
}
/// 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() {
return None
return None;
}
let date = DATE_REGEX.captures(&message)?;
let (date, message) = (
date.get(1)?.as_str().to_string(),
date.get(2)?.as_str().to_string(),
date.get(1)?.as_str().to_string(),
date.get(2)?.as_str().to_string(),
);
let message = message
@ -254,11 +271,14 @@ pub fn parse_message(message: String) -> Option<(String, Option<String>, String,
.to_string();
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 {
(None, message)
};
let (message, nick) = match find_username_color(&message) {
Some((name, content, color)) => (content, Some((name, color))),
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)> {
for (re, color) in COLORED_USERNAMES.iter() {
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

View File

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

View File

@ -1,15 +1,21 @@
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::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
use clap::Parser;
use clap::Parser;
fn main() {
#[cfg(feature = "winapi")]
unsafe { winapi::um::wincon::FreeConsole() };
unsafe {
winapi::um::wincon::FreeConsole()
};
let args = Args::parse();
let config_path = get_config_path();
if args.config_path {
@ -20,26 +26,25 @@ fn main() {
let mut config = load_config(config_path);
if args.read_messages {
let mut stream = connect(&config.host, config.proxy.clone()).expect("Error reading message");
let mut stream =
connect(&config.host, config.proxy.clone()).expect("Error reading message");
print!("{}", read_messages(
&mut stream,
config.max_messages,
0,
false
)
.ok().flatten()
.expect("Error reading messages").0.join("\n")
print!(
"{}",
read_messages(&mut stream, config.max_messages, 0, false)
.ok()
.flatten()
.expect("Error reading messages")
.0
.join("\n")
);
}
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(
&mut stream,
message
).expect("Error sending message");
send_message(&mut stream, message).expect("Error sending message");
}
if args.send_message.is_some() || args.read_messages {
@ -47,7 +52,7 @@ fn main() {
}
args.patch_config(&mut config);
let ctx = Arc::new(Context::new(&config));
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 socks::Socks5Stream;
@ -13,28 +19,44 @@ pub trait Stream: Read + Write + Unpin + Send + Sync + Debug {
}
impl Stream for TcpStream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); }
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); }
fn set_read_timeout(&self, timeout: Duration) {
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 {
fn set_read_timeout(&self, timeout: Duration) { 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)); }
fn set_read_timeout(&self, timeout: Duration) {
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> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
fn set_read_timeout(&self, timeout: Duration) {
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>> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); }
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); }
fn set_read_timeout(&self, timeout: Duration) {
self.get_ref().set_read_timeout(timeout);
}
fn set_write_timeout(&self, timeout: Duration) {
self.get_ref().set_write_timeout(timeout);
}
}
pub enum RacStream {
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"))` \
@ -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 (host, _) = url.split_once("/").unwrap_or((url, ""));
match scheme.to_lowercase().as_str() {
"rac" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42666")
},
false, false
))
},
"racs" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42667")
},
true, false
))
},
"wrac" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52666")
},
false, true
))
},
"wracs" => {
Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52667")
},
true, true
))
},
"rac" => Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42666")
},
false,
false,
)),
"racs" => Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:42667")
},
true,
false,
)),
"wrac" => Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52666")
},
false,
true,
)),
"wracs" => Some((
if host.contains(":") {
host.to_string()
} else {
format!("{host}:52667")
},
true,
true,
)),
_ => None,
}
}
@ -115,12 +133,18 @@ pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
/// proxy - socks5 proxy (host, (user, pass))
/// wrac - to use wrac protocol
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 {
if let Some((proxy, auth)) = parse_socks5_url(&proxy) {
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 {
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());
}
} 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)?)
};
let stream = if ssl {
let ip: String = host.split_once(":")
let ip: String = host
.split_once(":")
.map(|o| o.0.to_string())
.unwrap_or(host.clone());
Box::new(TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?
.connect(&ip, stream)?)
Box::new(
TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?
.connect(&ip, stream)?,
)
} else {
stream
};
@ -152,8 +182,8 @@ pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn E
if wrac {
let (client, _) = tungstenite::client(
&format!("ws{}://{host}", if ssl { "s" } else { "" }),
stream
&format!("ws{}://{host}", if ssl { "s" } else { "" }),
stream,
)?;
Ok(RacStream::WRAC(client))
} else {
@ -171,8 +201,13 @@ pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn E
/// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message)
/// }
pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str) -> Result<(), Box<dyn Error>> {
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
pub fn send_message_spoof_auth(
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 f != 0 {
@ -185,18 +220,14 @@ pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str) -> Result<
Ok(())
}
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(
stream: &mut RacStream,
message: &str
) -> Result<(), Box<dyn Error>> {
pub fn send_message(stream: &mut RacStream, message: &str) -> Result<(), Box<dyn Error>> {
match stream {
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
pub fn register_user(
stream: &mut RacStream,
name: &str,
password: &str
stream: &mut RacStream,
name: &str,
password: &str,
) -> Result<bool, Box<dyn Error>> {
match stream {
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 2 if the password is incorrect
pub fn send_message_auth(
stream: &mut RacStream,
name: &str,
password: &str,
message: &str
stream: &mut RacStream,
name: &str,
password: &str,
message: &str,
) -> Result<u8, Box<dyn Error>> {
match stream {
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)
pub fn read_messages(
stream: &mut RacStream,
max_messages: usize,
stream: &mut RacStream,
max_messages: usize,
last_size: usize,
chunked: bool
chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
match stream {
RacStream::WRAC(websocket) => wrac::read_messages(websocket, max_messages, last_size, chunked),
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, chunked)
RacStream::WRAC(websocket) => {
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
///
@ -18,9 +21,9 @@ pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box<dy
///
/// returns whether the user was registered
pub fn register_user(
stream: &mut (impl Write + Read),
name: &str,
password: &str
stream: &mut (impl Write + Read),
name: &str,
password: &str,
) -> Result<bool, Box<dyn Error>> {
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
if let Ok(out) = skip_null(stream) {
@ -42,9 +45,9 @@ pub fn register_user(
/// returns 1 if the user does not exist
/// returns 2 if the password is incorrect
pub fn send_message_auth(
stream: &mut (impl Write + Read),
name: &str,
password: &str,
stream: &mut (impl Write + Read),
name: &str,
password: &str,
message: &str,
) -> Result<u8, Box<dyn Error>> {
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];
stream.read_exact(&mut buf)?;
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
pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
while vec.ends_with(&[0]) {
vec.remove(vec.len()-1);
vec.remove(vec.len() - 1);
}
Ok(())
}
@ -83,10 +86,10 @@ pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut (impl Read + Write),
max_messages: usize,
last_size: usize,
chunked: bool
stream: &mut (impl Read + Write),
max_messages: usize,
last_size: usize,
chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write_all(&[0x00])?;
@ -123,10 +126,16 @@ pub fn read_messages(
let packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter()
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 })
let lines: Vec<String> = lines
.clone()
.into_iter()
.skip(if lines.len() >= max_messages {
lines.len() - max_messages
} else {
0
})
.map(|o| o.to_string())
.collect();
Ok(Some((lines, packet_size)))
}
}

View File

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