refactor: more avatar-specific stuff

This commit is contained in:
MeexReay 2025-06-30 01:38:04 +03:00
parent 00cc5b2e86
commit 5266d1190f
5 changed files with 157 additions and 40 deletions

8
Cargo.lock generated
View File

@ -244,7 +244,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "bRAC" name = "bRAC"
version = "0.1.5+2.0" version = "0.1.6+2.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -720,6 +720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -788,8 +789,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@ -2030,7 +2034,9 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util",
"h2", "h2",
"http", "http",
"http-body", "http-body",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bRAC" name = "bRAC"
version = "0.1.5+2.0" version = "0.1.6+2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -21,7 +21,7 @@ notify-rust = { version = "4.11.7", optional = true }
gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] } winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
tungstenite = "0.27.0" tungstenite = "0.27.0"
reqwest = "0.12.20" reqwest = { version = "0.12.20", features = ["blocking"] }
[build-dependencies] [build-dependencies]
winresource = { version = "0.1.20", optional = true } winresource = { version = "0.1.20", optional = true }

View File

@ -63,6 +63,8 @@ pub struct Config {
pub new_ui_enabled: bool, pub new_ui_enabled: bool,
#[serde(default)] #[serde(default)]
pub debug_logs: bool, pub debug_logs: bool,
#[serde(default)]
pub avatar: Option<String>,
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -154,6 +156,8 @@ pub struct Args {
#[arg(long)] #[arg(long)]
pub proxy: Option<String>, pub proxy: Option<String>,
#[arg(long)] #[arg(long)]
pub avatar: Option<String>,
#[arg(long)]
pub debug_logs: bool, pub debug_logs: bool,
} }
@ -207,6 +211,9 @@ impl Args {
if let Some(v) = self.new_ui_enabled { if let Some(v) = self.new_ui_enabled {
config.new_ui_enabled = v config.new_ui_enabled = v
} }
if let Some(v) = self.avatar.clone() {
config.avatar = Some(v)
}
if self.debug_logs { if self.debug_logs {
config.debug_logs = true config.debug_logs = true
} }

View File

@ -1,6 +1,11 @@
// TODO: REFACTOR THIS SHIT!!!!!!!!!!!!!!!
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::hash::{DefaultHasher, Hasher};
use std::sync::atomic::AtomicU64;
use std::sync::RwLockWriteGuard;
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock}; use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
use std::thread; use std::thread;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@ -25,6 +30,8 @@ use gtk::{
Orientation, Overlay, Picture, ScrolledWindow, Settings, Window, Orientation, Overlay, Picture, ScrolledWindow, Settings, Window,
}; };
use crate::chat::grab_avatar;
use super::{ use super::{
config::{ config::{
default_konata_size, default_max_messages, default_oof_update_time, default_update_time, default_konata_size, default_max_messages, default_oof_update_time, default_update_time,
@ -46,6 +53,7 @@ struct UiModel {
notifications: Arc<RwLock<Vec<String>>>, notifications: Arc<RwLock<Vec<String>>>,
default_avatar: Pixbuf, default_avatar: Pixbuf,
avatars: Arc<RwLock<HashMap<u64, Pixbuf>>>, avatars: Arc<RwLock<HashMap<u64, Pixbuf>>>,
latest_sign: Arc<AtomicU64>
} }
thread_local!( thread_local!(
@ -176,6 +184,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
let message_format_entry = let message_format_entry =
gui_entry_setting!("Message Format", message_format, ctx, settings_vbox); 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 avatar_entry = gui_option_entry_setting!("Avatar", avatar, ctx, settings_vbox);
let update_time_entry = let update_time_entry =
gui_usize_entry_setting!("Update Time", update_time, ctx, settings_vbox); gui_usize_entry_setting!("Update Time", update_time, ctx, settings_vbox);
let oof_update_time_entry = gui_usize_entry_setting!( let oof_update_time_entry = gui_usize_entry_setting!(
@ -257,6 +266,8 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
remove_gui_shit_entry, remove_gui_shit_entry,
#[weak] #[weak]
new_ui_enabled_entry, new_ui_enabled_entry,
#[weak]
avatar_entry,
move |_| { move |_| {
let config = Config { let config = Config {
host: host_entry.text().to_string(), host: host_entry.text().to_string(),
@ -269,6 +280,15 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
Some(name) Some(name)
} }
}, },
avatar: {
let avatar = avatar_entry.text().to_string();
if avatar.is_empty() {
None
} else {
Some(avatar)
}
},
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();
@ -380,12 +400,15 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
remove_gui_shit_entry, remove_gui_shit_entry,
#[weak] #[weak]
new_ui_enabled_entry, new_ui_enabled_entry,
#[weak]
avatar_entry,
move |_| { move |_| {
let config = Config::default(); let config = Config::default();
ctx.set_config(&config); ctx.set_config(&config);
save_config(get_config_path(), &config); save_config(get_config_path(), &config);
host_entry.set_text(&config.host); host_entry.set_text(&config.host);
name_entry.set_text(&config.name.unwrap_or_default()); name_entry.set_text(&config.name.unwrap_or_default());
avatar_entry.set_text(&config.avatar.unwrap_or_default());
proxy_entry.set_text(&config.proxy.unwrap_or_default()); proxy_entry.set_text(&config.proxy.unwrap_or_default());
message_format_entry.set_text(&config.message_format); message_format_entry.set_text(&config.message_format);
update_time_entry.set_text(&config.update_time.to_string()); update_time_entry.set_text(&config.update_time.to_string());
@ -791,6 +814,7 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
notifications: Arc::new(RwLock::new(Vec::<String>::new())), notifications: Arc::new(RwLock::new(Vec::<String>::new())),
default_avatar: load_pixbuf(include_bytes!("images/avatar.png")).unwrap(), default_avatar: load_pixbuf(include_bytes!("images/avatar.png")).unwrap(),
avatars: Arc::new(RwLock::new(HashMap::new())), avatars: Arc::new(RwLock::new(HashMap::new())),
latest_sign: Arc::new(AtomicU64::new(0))
} }
} }
@ -843,6 +867,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
move || { move || {
while let Ok((messages, clear)) = receiver.recv() { while let Ok((messages, clear)) = receiver.recv() {
let ctx = ctx.clone(); let ctx = ctx.clone();
timeout_add_once(Duration::ZERO, move || { timeout_add_once(Duration::ZERO, move || {
GLOBAL.with(|global| { GLOBAL.with(|global| {
if let Some(ui) = &*global.borrow() { if let Some(ui) = &*global.borrow() {
@ -852,6 +877,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
} }
} }
for message in messages.iter() { for message in messages.iter() {
prepare_avatar(&mut ui.avatars.write().unwrap(), message); // TODO: fuck
on_add_message(ctx.clone(), &ui, message.to_string(), !clear); on_add_message(ctx.clone(), &ui, message.to_string(), !clear);
} }
} }
@ -1036,8 +1062,34 @@ fn get_message_box(
hbox hbox
} }
fn load_avatar(ui: &UiModel, url: &str) -> Option<Pixbuf> { fn prepare_avatar(avatars: &mut RwLockWriteGuard<'_, HashMap<u64, Pixbuf>>, message: &str) {
Some(ui.default_avatar.clone()) if let Some(url) = grab_avatar(message) {
let mut hasher = DefaultHasher::new();
hasher.write(url.as_bytes());
let id = hasher.finish();
if !avatars.contains_key(&id) {
let Ok(data) = reqwest::blocking::get(&url).and_then(|o| o.bytes()) else {
return
};
let Ok(pixbuf) = load_pixbuf(&data.to_vec()) else {
return
};
avatars.insert(id, pixbuf);
}
}
}
fn get_avatar_or_default(ui: &UiModel, url: &str) -> Pixbuf {
let mut hasher = DefaultHasher::new();
hasher.write(url.as_bytes());
let id = hasher.finish();
if let Some(pixbuf) = ui.avatars.read().unwrap().get(&id) {
pixbuf.clone() // FIXME: cloning pixbufs is a dangerous shit
} else {
ui.default_avatar.clone()
}
} }
fn get_new_message_box( fn get_new_message_box(
@ -1045,7 +1097,7 @@ fn get_new_message_box(
ui: &UiModel, ui: &UiModel,
message: String, message: String,
notify: bool, notify: bool,
formatting_enabled: bool, formatting_enabled: bool
) -> Overlay { ) -> Overlay {
// TODO: softcode these colors // TODO: softcode these colors
@ -1055,10 +1107,13 @@ fn get_new_message_box(
("#585858", "#292929", "#000000") ("#585858", "#292929", "#000000")
}; };
let latest_sign = ui.latest_sign.load(Ordering::SeqCst);
let (date, ip, content, name, color, avatar) = let (date, ip, content, name, color, avatar) =
if let (true, Some((date, ip, content, nick, avatar))) = if let (true, Some((date, ip, content, nick, avatar))) =
(formatting_enabled, parse_message(message.clone())) (formatting_enabled, parse_message(message.clone()))
{ {
( (
date, date,
ip, ip,
@ -1070,7 +1125,7 @@ fn get_new_message_box(
.map(|o| o.1.to_string()) .map(|o| o.1.to_string())
.unwrap_or("#DDDDDD".to_string()), .unwrap_or("#DDDDDD".to_string()),
avatar avatar
.and_then(|o| load_avatar(ui, &o)) .map(|o| get_avatar_or_default(ui, &o))
.unwrap_or(ui.default_avatar.clone()), .unwrap_or(ui.default_avatar.clone()),
) )
} else { } else {
@ -1084,9 +1139,32 @@ fn get_new_message_box(
) )
}; };
if notify && !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(
ctx.clone(),
ui,
&if name == "System" {
"System Message".to_string()
} else {
format!("{}'s Message", name)
},
&glib::markup_escape_text(&content),
);
}
}
let sign = get_message_sign(&name, &date);
let squashed = latest_sign == sign;
ui.latest_sign.store(sign, Ordering::SeqCst);
let overlay = Overlay::new(); let overlay = Overlay::new();
if !squashed {
let fixed = Fixed::new(); let fixed = Fixed::new();
fixed.set_can_target(false);
let avatar_picture = Picture::for_pixbuf(&avatar); let avatar_picture = Picture::for_pixbuf(&avatar);
avatar_picture.set_css_classes(&["message-avatar"]); avatar_picture.set_css_classes(&["message-avatar"]);
@ -1099,9 +1177,11 @@ fn get_new_message_box(
fixed.put(&avatar_picture, 0.0, 4.0); fixed.put(&avatar_picture, 0.0, 4.0);
overlay.add_overlay(&fixed); overlay.add_overlay(&fixed);
}
let vbox = GtkBox::new(Orientation::Vertical, 2); let vbox = GtkBox::new(Orientation::Vertical, 2);
if !squashed {
vbox.append(&Label::builder() vbox.append(&Label::builder()
.label(format!( .label(format!(
"<span color=\"{color}\">{}</span> <span color=\"{date_color}\">{}</span> <span color=\"{ip_color}\">{}</span>", "<span color=\"{color}\">{}</span> <span color=\"{date_color}\">{}</span> <span color=\"{ip_color}\">{}</span>",
@ -1115,8 +1195,8 @@ fn get_new_message_box(
.wrap(true) .wrap(true)
.wrap_mode(WrapMode::WordChar) .wrap_mode(WrapMode::WordChar)
.use_markup(true) .use_markup(true)
.vexpand(true)
.build()); .build());
}
vbox.append(&Label::builder() vbox.append(&Label::builder()
.label(format!( .label(format!(
@ -1129,20 +1209,31 @@ fn get_new_message_box(
.wrap(true) .wrap(true)
.wrap_mode(WrapMode::WordChar) .wrap_mode(WrapMode::WordChar)
.use_markup(true) .use_markup(true)
.vexpand(true)
.build()); .build());
vbox.set_valign(Align::Fill);
vbox.set_vexpand(true);
vbox.set_margin_start(37); vbox.set_margin_start(37);
vbox.set_hexpand(true);
overlay.set_child(Some(&vbox)); overlay.set_child(Some(&vbox));
if !squashed {
overlay.set_margin_top(7); overlay.set_margin_top(7);
} else {
overlay.set_margin_top(2);
}
overlay overlay
} }
// creates sign that expires in 0-20 minutes
fn get_message_sign(name: &str, date: &str) -> u64 {
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
hasher.write(date[..date.len()-2].as_bytes());
hasher.finish()
}
/// returns message sign
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 notify = notify && ctx.config(|c| c.notifications_enabled); let notify = notify && ctx.config(|c| c.notifications_enabled);
@ -1164,7 +1255,7 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
ui.chat_box.append(&get_new_message_box(ctx.clone(), ui, message, notify, formatting_enabled)); ui.chat_box.append(&get_new_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
} else { } else {
ui.chat_box.append(&get_message_box(ctx.clone(), ui, message, notify, formatting_enabled)); ui.chat_box.append(&get_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
} };
timeout_add_local_once(Duration::from_millis(1000), move || { timeout_add_local_once(Duration::from_millis(1000), move || {
GLOBAL.with(|global| { GLOBAL.with(|global| {

View File

@ -34,7 +34,7 @@ lazy_static! {
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap(); pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
pub static ref AVATAR_REGEX: Regex = Regex::new(r"(.*) \x06!!AR!!(.*)").unwrap(); pub static ref AVATAR_REGEX: Regex = Regex::new(r"(.*)\x06!!AR!!(.*)").unwrap();
pub static ref DEFAULT_USER_AGENT: Regex = Regex::new(r"<(.*?)> (.*)").unwrap(); pub static ref DEFAULT_USER_AGENT: Regex = Regex::new(r"<(.*?)> (.*)").unwrap();
@ -234,13 +234,17 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
if message.starts_with("/") && ctx.config(|o| o.commands_enabled) { if message.starts_with("/") && ctx.config(|o| o.commands_enabled) {
on_command(ctx.clone(), &message)?; on_command(ctx.clone(), &message)?;
} else { } else {
let message = prepare_message( let mut 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(avatar) = ctx.config(|o| o.avatar.clone()) {
message = format!("{message}\x06!!AR!!{avatar}"); // TODO: softcode this shittttttt
}
if let Some(password) = ctx.registered.read().unwrap().clone() { if let Some(password) = ctx.registered.read().unwrap().clone() {
send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message)?; send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message)?;
} else { } else {
@ -257,6 +261,15 @@ pub fn sanitize_message(message: String) -> Option<String> {
Some(message) Some(message)
} }
/// message -> avatar
pub fn grab_avatar(message: &str) -> Option<String> {
if let Some(message) = AVATAR_REGEX.captures(&message) {
Some(message.get(2)?.as_str().to_string())
} else {
None
}
}
/// message -> (date, ip, text, (name, color), avatar) /// message -> (date, ip, text, (name, color), avatar)
pub fn parse_message( pub fn parse_message(
message: String, message: String,