Compare commits

...

63 Commits

Author SHA1 Message Date
fa75ca60c4 rac urls and change nicks colors 2025-04-23 00:39:49 +03:00
091c1bca03 flush websocket 2025-04-22 00:27:17 +03:00
73f7c565e1 wrac compatibility (no tests) 2025-04-21 21:24:41 +03:00
06c27aac63 wym you build project in install.sh? 2025-04-21 20:27:32 +03:00
2a3473853b install and uninstall bat 2025-04-21 20:23:45 +03:00
e55d5a7a55 notifications setting and now all gnotifications hide 2025-04-21 20:01:31 +03:00
cb17464d8f desktop files and winapi usage 2025-04-21 16:47:42 +03:00
015d8fca59 some optimizations 2025-04-21 01:18:20 +03:00
cc01dd1e49 close libnotify notifications on active 2025-04-21 01:14:13 +03:00
cdbe45254d libnotify 2025-04-21 00:49:06 +03:00
88b66af84e move read messages error to println 2025-04-20 23:14:37 +03:00
233a5eb9d5 change name colors 2025-04-20 19:12:28 +03:00
f08f97f267 notifications 2025-04-19 23:54:45 +03:00
18dabe1146 makefile upd 2025-04-19 18:42:06 +03:00
ba54300e6c use std::env for windows 2025-04-19 15:32:09 +03:00
8f539713f6 some chunked reading fixes 2025-04-19 18:24:42 +03:00
c3fd29812f ver 0.1.4+2.0 2025-04-19 18:04:30 +03:00
720cfe0e5b remove redudant 2025-04-19 16:16:47 +03:00
e3e9af505f remove dbg 2025-04-19 16:14:20 +03:00
822c03c1b4 resizable 2025-04-19 16:14:08 +03:00
11e1991ccf settings macros 2025-04-19 16:08:14 +03:00
998d8025f8 socks5 proxy and some sugoma-specified stuff 2025-04-19 15:50:03 +03:00
20d424b8d5 faq 2025-04-19 14:03:36 +03:00
30133c1198 connect to server on click in server list 2025-04-19 13:27:03 +03:00
d8170d10e5 makefile update 2025-04-19 11:34:32 +03:00
123b750e78 reset button 2025-04-19 02:38:47 +03:00
bda976bd1b move protocol out of gui 2025-04-19 02:26:10 +03:00
588e536077 gui settings 2025-04-19 02:22:14 +03:00
94680c95e1 ask name fix and some refactor 2025-04-19 01:24:07 +03:00
5c1f8f0cae Merge branch 'main' into gui 2025-04-19 00:52:17 +03:00
e8032b665a make file tarmosheniye 2025-04-19 00:51:34 +03:00
592874680c config refactor and some fixes like always 2025-04-18 23:08:10 +03:00
8dba6f2fd9 readme update 2025-04-17 23:56:55 +03:00
5dd7dc0fdc some fixes ok 2025-04-17 23:54:44 +03:00
ebd8d15879 copy image.png and logo.gif for readme 2025-04-17 23:40:47 +03:00
e3b3b09fe2 remove images from root 2025-04-17 23:38:29 +03:00
dbc6323ab6 gui init commit 2025-04-17 23:36:09 +03:00
bc89528be6 fix readme logo 2025-04-17 22:59:18 +03:00
173b5bd217 try to merge 2025-04-17 22:58:16 +03:00
47baed7d6f move images to assets/ 2025-04-17 22:56:44 +03:00
MeexReay
7af3cd11f3
brac logo 2025-04-17 20:11:06 +03:00
MeexReay
ee86ec8cfe
Update README.md 2025-04-17 20:05:59 +03:00
08c8a25966 oops 2025-04-17 00:43:50 +03:00
b65e6af93a todo in args 2025-04-17 00:41:55 +03:00
67a48b74f9 ver 0.1.3+2.0 2025-04-16 21:44:45 +03:00
5508d22608 fix some windows gtk stuff 2025-04-16 21:40:09 +03:00
701591e5be win conf fix x3 2025-04-16 20:50:00 +03:00
e5e3dfe098 another windows config fix 2025-04-16 20:49:20 +03:00
e804712aa0 windows config fix 2025-04-16 20:45:47 +03:00
bc42d8815b minimal chat fix 2025-04-16 20:11:47 +03:00
13a45d9562 remove shit 2025-04-16 20:00:09 +03:00
a85442f57d gui as default and more ui 2025-04-16 19:51:37 +03:00
904ac31a97 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-04-16 14:30:20 +03:00
26ebf5a3cb da 2025-04-16 14:30:14 +03:00
c77d52e242 light theme 2025-04-16 03:48:40 +03:00
4f78d48b10 fix flake and scroll 2025-04-16 03:09:53 +03:00
e90b94a694 flake gtk features change 2025-04-16 02:45:51 +03:00
c6fe8658d9 flake update 2025-04-16 02:39:09 +03:00
6e5d050de3 disable gtk as default 2025-04-16 02:24:13 +03:00
c1f6afe563 fix deps 2025-04-16 02:21:59 +03:00
440d6c72d8 gtk gui 2025-04-16 02:21:26 +03:00
b99311bd7c register and login command and some fixes i apologize 2025-04-15 02:15:05 +03:00
41340f0f45 remove empty messages and allow non-format messages 2025-04-14 22:12:44 +03:00
40 changed files with 3291 additions and 1590 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
/target /target
/result /result
/config.yml /build
/config.yml
/bRAC

1131
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,30 @@
[package] [package]
name = "bRAC" name = "bRAC"
version = "0.1.2+2.0" version = "0.1.4+2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
rand = "0.9.0" rand = "0.9.1"
regex = "1.11.1" regex = "1.11.1"
colored = "3.0.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
serde_yml = "0.0.12"
homedir = "0.3.4"
native-tls = "0.2.14"
clap = { version = "4.5.36", features = ["derive"] } clap = { version = "4.5.36", features = ["derive"] }
serde = { version = "1.0.219", features = ["serde_derive"] } serde = { version = "1.0.219", features = ["serde_derive"] }
serde_yml = "0.0.12" gtk4 = { version = "0.9.6", features = [ "v4_10" ] }
crossterm = { version = "0.29.0", optional = true } chrono = "0.4.40"
homedir = { version = "0.3.4", optional = true } serde_default = "0.2.0"
native-tls = { version = "0.2.14", optional = true } socks = "0.3.4"
libnotify = { version = "1.0.3", optional = true }
gdk-pixbuf = { version = "0.3.0", optional = true }
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
tungstenite = "0.26.2"
[features] [features]
default = ["ssl", "pretty", "homedir"] default = []
ssl = ["dep:native-tls"] libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
pretty = ["dep:crossterm"] winapi = ["dep:winapi"]
homedir = ["dep:homedir"]
[build-dependencies]
winresource = "0.1.20"

36
Makefile Normal file
View File

@ -0,0 +1,36 @@
.PHONY: clean build windows build
build: build/windows-x86_64 build/linux-x86_64
windows: build/windows-x86_64
linux: build/linux-x86_64
build/windows-x86_64:
mkdir -p build
mkdir -p $@
cargo build -r -F winapi --target x86_64-pc-windows-gnu
curl -s https://api.github.com/repos/wingtk/gvsbuild/releases/latest \
| grep -o ".*browser_download_url.*GTK4_Gvsbuild.*_x64.zip.*" \
| cut -d : -f 2,3 \
| tr -d \" \
| wget -O $@/gtk4.zip -qi -
unzip $@/gtk4.zip -d $@
rm $@/gtk4.zip
mv $@/bin/* $@/
cp target/x86_64-pc-windows-gnu/release/bRAC.exe $@
rm -r $@/bin
cp install.bat $@
cp uninstall.bat $@
build/linux-x86_64:
mkdir -p build
mkdir -p $@
cargo build -r --target x86_64-unknown-linux-gnu
# patchbin target/x86_64-unknown-linux-gnu/release/bRAC
cp target/x86_64-unknown-linux-gnu/release/bRAC $@
cp ru.themixray.bRAC.png $@
cp ru.themixray.bRAC.desktop $@
cp install.sh $@
cp uninstall.sh $@
clean:
rm -r build

View File

@ -1,4 +1,4 @@
# bRAC # ![logo](logo.gif)
<!-- <!--
[<img src="https://github.com/user-attachments/assets/f2be5caa-6246-4a6a-9bee-2b53086f9afb" height="30">]() [<img src="https://github.com/user-attachments/assets/f2be5caa-6246-4a6a-9bee-2b53086f9afb" height="30">]()
[<img src="https://github.com/user-attachments/assets/4d35191d-1dbc-4391-a761-6ae7f76ba7af" height="30">]() [<img src="https://github.com/user-attachments/assets/4d35191d-1dbc-4391-a761-6ae7f76ba7af" height="30">]()
@ -9,17 +9,18 @@ better RAC client
## features ## features
- cheat commands (type /help) - gtk4 GUI
- no ip and date visible
- uses TOR proxy server by default (meex.lol:11234)
- plays sound when users receive your messages
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
- configurable message format
- RACv1.99.x and RACv2.0 compatible - RACv1.99.x and RACv2.0 compatible
- chat commands (type /help)
- no ip and date visible for anyone
- uses TOR proxy server by default (meex.lol:11234)
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
- many command-line options (--help)
- rich configuration (--config-path to get file path and --configure to edit)
- RACS compatible (--enable-ssl or in --configure enable SSL) - RACS compatible (--enable-ssl or in --configure enable SSL)
- chunked reading messages - chunked reading messages
![image](https://github.com/user-attachments/assets/a2858662-50f1-4554-949c-f55addf48fcc) ![screenshot](image.png)
## how to run ## how to run
@ -37,17 +38,10 @@ git clone https://github.com/MeexReay/bRAC.git
cd bRAC cd bRAC
``` ```
3. Build or run with Cargo 3. Run with Cargo
```bash ```bash
cargo build --release # build release (target/release/bRAC) cargo build -r # build release (target/release/bRAC)
cargo run --release # run (builds and runs bRAC itself) cargo run -r # build and run
```
Minimal version:
```bash
cargo build --release --no-default-features
cargo run --release --no-default-features
``` ```
### nix package ### nix package
@ -55,70 +49,28 @@ cargo run --release --no-default-features
If you have Nix package manager installed, you can use: If you have Nix package manager installed, you can use:
```bash ```bash
nix build github:MeexReay/bRAC # build binary (result/bin/bRAC) nix build github:MeexReay/bRAC # build release (result/bin/bRAC)
nix run github:MeexReay/bRAC # run (builds and runs bRAC) nix run github:MeexReay/bRAC # build and run
``` ```
Minimal version: ## chat commands
```bash
nix build github:MeexReay/bRAC#bRAC-minimal
nix run github:MeexReay/bRAC#bRAC-minimal
```
## default config
get config path - `bRAC --config-path` \
reconfigure client - `bRAC --configure`
```yml
host: meex.lol:11234 # server host
name: null # user name (null - ask every time)
message_format: 리㹰<{name}> {text} # message format
update_time: 50 # update chat interval
max_messages: 200 # chat messages limit
enable_ip_viewing: true # enable users' ip viewing
disable_ip_hiding: false # disable your ip hiding
enable_auth: true # enable auth-mode
enable_ssl: false # enable ssl connection
enable_chunked: true # enable chunked reading
```
## command-line options
```
-p, --config-path Print config path
-H, --host <HOST> Use specified host
-n, --name <NAME> Use specified name
-F, --message-format <MESSAGE_FORMAT> Use specified message format
-r, --read-messages Print unformatted messages from chat and exit
-s, --send-message <MESSAGE> Send unformatted message to chat and exit
-f, --disable-formatting Disable message formatting and sanitizing
-c, --disable-commands Disable slash commands
-i, --disable-ip-hiding Disable ip hiding
-v, --enable-users-ip-viewing Enable users IP viewing
-C, --configure Configure client
-a, --enable-auth Enable authentication
-S, --enable-ssl Enable SSL
-u, --enable-chunked Enable chunked reading
-h, --help Print help
-V, --version Print version
```
## cheat commands
commands are any messages that start with a slash `/` \ commands are any messages that start with a slash `/` \
messages starting with a slash are sent to chat only if the `--disable-commands` option is specified messages starting with a slash are sent to chat only if the `--disable-commands` option is specified
- `/help` - show help message - `/help` - show help message
- `/register password` - try to register account
- `/login password` - login to account
- `/clear` - clear chat - `/clear` - clear chat
- `/spam *args` - spam with text - `/spam *args` - spam with text
- `/ping` - get server ping (send + read) - `/ping` - get server ping (send + read)
## docs ## docs
- [Message formats](https://github.com/MeexReay/bRAC/blob/main/docs/message_formats.md) - [Message formats](docs/message_formats.md)
- [Authenticated mode](https://github.com/MeexReay/bRAC/blob/main/docs/auth_mode.md) - [Authenticated mode](docs/auth_mode.md)
- [Cross compile](docs/cross_compile.md)
- [FAQ](docs/faq.md)
## see also ## see also

16
build.rs Normal file
View File

@ -0,0 +1,16 @@
use {
std::{
env,
io,
},
winresource::WindowsResource,
};
fn main() -> io::Result<()> {
if env::var_os("CARGO_CFG_WINDOWS").is_some() {
WindowsResource::new()
.set_icon("icon.ico")
.compile()?;
}
Ok(())
}

15
docs/cross_compile.md Normal file
View File

@ -0,0 +1,15 @@
# Cross-compile on Linux to Windows
## Install dev packages
on Nix:
```bash
nix-shell -p pkgsCross.mingwW64.stdenv.cc pkgsCross.mingwW64.windows.pthreads pkgsCross.mingwW64.gtk4
```
## Build
```bash
build build/windows-x86_64
```

7
docs/faq.md Normal file
View File

@ -0,0 +1,7 @@
# FAQ
## What is RAC protocol
Sugomas “IRC killer”, the so-called RAC (Real Address Chat) protocol. (The worst name for a protocol.)
[*brought from here*](https://bedohswe.eu.org/text/rac/protocol.md.html)

45
flake.lock generated
View File

@ -18,6 +18,23 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1744463964, "lastModified": 1744463964,
@ -51,11 +68,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1736320768, "lastModified": 1744536153,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -68,6 +85,7 @@
"root": { "root": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
@ -77,11 +95,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1744513456, "lastModified": 1744684506,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=", "narHash": "sha256-pDPDMT1rdkTWi8MIoZ67gT3L817R7P0Jo+PP+BrnyJI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2", "rev": "47beae969336c05e892e1e4a9dbaac9593de34ab",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,6 +107,21 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -7,67 +7,37 @@
rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.url = "github:oxalica/rust-overlay";
}; };
outputs = inputs: outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
inputs.flake-parts.lib.mkFlake { inherit inputs; } { flake-utils.lib.eachDefaultSystem (system:
systems = [ "x86_64-linux" ]; let
perSystem = { config, self', pkgs, lib, system, ... }: devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
let cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
devDeps = with pkgs; [ pkg-config openssl ]; overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); inherit system overlays;
msrv = cargoToml.package.rust-version;
rustPackage = { version, features, deps }:
(pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
}).buildRustPackage {
inherit (cargoToml.package) name;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
version = lib.concatStrings [ cargoToml.package.version version ];
buildNoDefaultFeatures = true;
buildFeatures = features;
buildInputs = deps;
nativeBuildInputs = deps;
patchPhase = ''
substituteInPlace Cargo.toml --replace \
'version = "${cargoToml.package.version}"' \
'version = "${lib.concatStrings [ cargoToml.package.version version ]}"'
'';
};
mkDevShell = rustc:
pkgs.mkShell {
shellHook = ''
export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc}
'';
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ rustc ];
};
in {
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ (import inputs.rust-overlay) ];
};
packages.default = self'.packages.bRAC;
devShells.default = self'.devShells.stable;
packages.bRAC = (rustPackage {
version = "";
features = "default";
deps = with pkgs; [ pkg-config openssl ];
});
packages.bRAC-minimal = (rustPackage {
version = "-minimal";
features = "";
deps = [];
});
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
devShells.stable = (mkDevShell pkgs.rust-bin.stable.latest.default);
devShells.msrv = (mkDevShell pkgs.rust-bin.stable.${msrv}.default);
}; };
}; mkDevShell = rustc:
} pkgs.mkShell {
shellHook = ''
export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc}
'';
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ rustc ];
};
in {
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
devShells.default = (mkDevShell pkgs.rust-bin.stable.latest.default);
packages.default = (pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
}).buildRustPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ pkgs.rustc ];
};
}
);
}

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

30
install.bat Normal file
View File

@ -0,0 +1,30 @@
@echo off
net session >nul 2>&1 || (
echo This script requires administrator privileges.
pause
exit /b
)
set "DEST=C:\Program Files\bRAC"
mkdir "%DEST%" 2>nul
xcopy "." "%DEST%\" /E /I /H /Y >nul
for /d %%u in ("C:\Users\*") do (
if exist "%%u\AppData\Roaming\Microsoft\Windows\Desktop" (
call :s "%%u\AppData\Roaming\Microsoft\Windows\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
) else if exist "%%u\Desktop" (
call :s "%%u\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
)
)
exit /b
:s
set "v=%TEMP%\_s.vbs"
> "%v%" echo Set o=CreateObject("WScript.Shell")
>>"%v%" echo Set l=o.CreateShortcut("%~1")
>>"%v%" echo l.TargetPath="%~2"
>>"%v%" echo l.WorkingDirectory="%~dp2"
>>"%v%" echo l.Save
wscript "%v%" >nul
del "%v%" >nul
exit /b

11
install.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
cp bRAC /bin/bRAC
chmod +x /bin/bRAC
cp ru.themixray.bRAC.png /usr/share/pixmaps
cp ru.themixray.bRAC.desktop /usr/share/applications

BIN
logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

12
ru.themixray.bRAC.desktop Normal file
View File

@ -0,0 +1,12 @@
[Desktop Entry]
Name=bRAC
Version=0.1.4
Type=Application
Comment=better RAC client
Icon=ru.themixray.bRAC.png
Exec=bRAC
Categories=Network;
StartupNotify=true
DBusActivatable=true
Terminal=false
X-GNOME-UsesNotifications=true

BIN
ru.themixray.bRAC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,270 +0,0 @@
use std::{
error::Error, io::{stdout, Write},
sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock},
time::{SystemTime, UNIX_EPOCH}
};
use colored::{Color, Colorize};
use super::{
proto::{connect, read_messages, send_message, send_message_spoof_auth},
util::sanitize_text,
config::Context
};
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
pub static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![
(Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), Color::Green), // bRAC
(Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed), // CRAB
(Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), Color::Magenta), // Mefidroniy
(Regex::new(r"<(.*?)> (.*)").unwrap(), Color::Cyan), // clRAC
];
}
#[cfg(not(feature = "pretty"))]
pub mod minimal_tui;
#[cfg(not(feature = "pretty"))]
pub use minimal_tui::run_main_loop;
#[cfg(feature = "pretty")]
pub mod pretty_tui;
#[cfg(feature = "pretty")]
pub use pretty_tui::run_main_loop;
pub struct ChatStorage {
messages: RwLock<Vec<String>>,
packet_size: AtomicUsize
}
impl ChatStorage {
pub fn new() -> Self {
ChatStorage {
messages: RwLock::new(Vec::new()),
packet_size: AtomicUsize::default()
}
}
pub fn packet_size(&self) -> usize {
self.packet_size.load(Ordering::SeqCst)
}
pub fn messages(&self) -> Vec<String> {
self.messages.read().unwrap().clone()
}
pub fn update(&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 {
messages.drain(max_length..);
}
*self.messages.write().unwrap() = messages;
}
pub fn append_and_store(&self, max_length: usize, messages: Vec<String>, packet_size: usize) {
self.packet_size.store(packet_size, Ordering::SeqCst);
self.append(max_length, messages);
}
pub fn append(&self, max_length: usize, messages: Vec<String>) {
self.messages.write().unwrap().append(&mut messages.clone());
if self.messages.read().unwrap().len() > max_length {
self.messages.write().unwrap().drain(max_length..);
}
}
}
const HELP_MESSAGE: &str = "Help message:\r
/help - show help message\r
/clear n - send empty message n times\r
/spam n text - send message with text n times\r
/ping - check server ping\r
\r
Press enter to close";
pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> {
let command = command.trim_start_matches("/");
let (command, args) = command.split_once(" ").unwrap_or((&command, ""));
let args = args.split(" ").collect::<Vec<&str>>();
let response = move || -> Option<String> {
if command == "clear" {
let times = args.get(0)?.parse().ok()?;
for _ in 0..times {
send_message(&mut connect(&ctx.host, ctx.enable_ssl).ok()?, "\r").ok()?;
}
None
} else if command == "spam" {
let times = args.get(0)?.parse().ok()?;
let msg = args[1..].join(" ");
for _ in 0..times {
send_message(&mut connect(&ctx.host, ctx.enable_ssl).ok()?, &("\r".to_string()+&msg)).ok()?;
}
None
// send_message(&mut connect(&ctx.host, ctx.enable_ssl)?,
// &prepare_message(ctx.clone(),
// &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(ctx.max_messages)
// ))?;
} else if command == "help" {
Some(HELP_MESSAGE.to_string())
} else if command == "ping" {
let mut before = ctx.messages.packet_size();
let message = format!("Checking ping... {:X}", SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_millis());
send_message(&mut connect(&ctx.host, ctx.enable_ssl).ok()?, &message).ok()?;
let start = SystemTime::now();
loop {
let data = read_messages(
&mut connect(&ctx.host, ctx.enable_ssl).ok()?,
ctx.max_messages,
before,
!ctx.enable_ssl,
ctx.enable_chunked
).ok().flatten();
if let Some((data, size)) = data {
if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) {
if last.contains(&message) {
break;
} else {
before = size;
}
} else {
before = size;
}
}
}
send_message(&mut connect(&ctx.host, ctx.enable_ssl).ok()?, &format!("Ping = {}ms", start.elapsed().unwrap().as_millis())).ok()?;
None
} else {
None
}
}();
if let Some(response) = response {
write!(stdout(), "{}", response)?;
stdout().flush()?;
}
Ok(())
}
pub fn prepare_message(context: Arc<Context>, message: &str) -> String {
format!("{}{}{}",
if !context.disable_hiding_ip {
"\r\x07"
} else {
""
},
message,
if !context.disable_hiding_ip {
let spaces = if context.enable_auth {
39
} else {
54
};
if message.chars().count() < spaces {
" ".repeat(spaces-message.chars().count())
} else {
String::new()
}
} else {
String::new()
}
)
}
pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
if message.starts_with("/") && !ctx.disable_commands {
on_command(ctx.clone(), &message)?;
} else {
let message = prepare_message(
ctx.clone(),
&ctx.message_format
.replace("{name}", &ctx.name)
.replace("{text}", &message)
);
if ctx.enable_auth {
send_message_spoof_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
} else {
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
}
}
Ok(())
}
pub fn format_message(enable_ip_viewing: bool, message: String) -> Option<String> {
let message = sanitize_text(&message);
let date = DATE_REGEX.captures(&message)?;
let (date, message) = (
date.get(1)?.as_str().to_string(),
date.get(2)?.as_str().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())
} else {
(None, message)
};
let message = message
.trim_start_matches("(UNREGISTERED)")
.trim_start_matches("(UNAUTHORIZED)")
.trim_start_matches("(UNAUTHENTICATED)")
.trim()
.to_string()+" ";
let prefix = if enable_ip_viewing {
if let Some(ip) = ip {
format!("{}{} [{}]", ip, " ".repeat(if 15 >= ip.chars().count() {15-ip.chars().count()} else {0}), date)
} else {
format!("{} [{}]", " ".repeat(15), date)
}
} else {
format!("[{}]", date)
};
Some(if let Some(captures) = find_username_color(&message) {
let nick = captures.0;
let content = captures.1;
let color = captures.2;
format!(
"{} {} {}",
prefix.white().dimmed(),
format!("<{}>", nick).color(color).bold(),
content.white().blink()
)
} else {
format!(
"{} {}",
prefix.white().dimmed(),
message.white().blink()
)
})
}
pub fn find_username_color(message: &str) -> Option<(String, String, Color)> {
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()))
}
}
None
}

143
src/chat/config.rs Normal file
View File

@ -0,0 +1,143 @@
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 { 50 }
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_max_messages")] pub max_messages: usize,
#[serde(default = "default_true")] pub hide_my_ip: bool,
#[serde(default)] pub show_other_ip: bool,
#[serde(default)] pub auth_enabled: bool,
#[serde(default)] pub ssl_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 wrac_enabled: bool,
#[serde(default)] pub proxy: Option<String>,
#[serde(default = "default_true")] pub notifications_enabled: bool,
}
pub fn get_config_path() -> PathBuf {
let mut config_dir = PathBuf::from_str(".").unwrap();
#[cfg(not(target_os = "windows"))]
if let Some(dir) = {
let home_dir = {
use homedir::my_home;
my_home().ok().flatten()
};
#[cfg(target_os = "linux")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
#[cfg(target_os = "macos")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
config_dir
} {
config_dir = dir;
}
#[cfg(target_os = "windows")]
if let Some(dir) = {
use std::env;
env::var("APPDATA")
.ok()
.and_then(|o| Some(PathBuf::from_str(&o).ok()?.join("bRAC")))
} {
config_dir = dir;
}
config_dir.join("config.yml")
}
pub fn load_config(path: PathBuf) -> Config {
if !fs::exists(&path).unwrap_or_default() {
let config = Config::default();
let config_text = serde_yml::to_string(&config).expect("Config save error");
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
fs::write(&path, config_text).expect("Config save error");
config
} else {
let config = &fs::read_to_string(&path).expect("Config load error");
serde_yml::from_str(config).expect("Config load error")
}
}
pub fn save_config(path: PathBuf, config: &Config) {
let config_text = serde_yml::to_string(config).expect("Config save error");
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
fs::write(&path, config_text).expect("Config save error");
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
/// Print config path
#[arg(short='p', long)]
pub config_path: bool,
/// Print unformatted messages from chat and exit
#[arg(short='r', long)]
pub read_messages: bool,
/// Send unformatted message to chat and exit
#[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 max_messages: 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 ssl_enabled: 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 wrac_enabled: Option<bool>,
#[arg(long)] pub proxy: Option<String>,
}
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.max_messages { config.max_messages = 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.auth_enabled { config.auth_enabled = v }
if let Some(v) = self.ssl_enabled { config.ssl_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 let Some(v) = self.wrac_enabled { config.wrac_enabled = v }
}
}

84
src/chat/ctx.rs Normal file
View File

@ -0,0 +1,84 @@
use std::sync::{atomic::{AtomicUsize, Ordering}, mpsc::Sender, Arc, RwLock};
use rand::random;
use super::config::Config;
pub struct Context {
pub registered: RwLock<Option<String>>,
pub config: RwLock<Config>,
pub sender: RwLock<Option<Arc<Sender<(String, bool)>>>>,
pub messages: RwLock<Vec<String>>,
pub packet_size: AtomicUsize,
pub name: RwLock<String>
}
impl Context {
pub fn new(config: &Config) -> Context {
Context {
registered: RwLock::new(None),
config: RwLock::new(config.clone()),
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>()))),
}
}
pub fn name(&self) -> String {
self.name.read().unwrap().clone()
}
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.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 {
map(&self.config.read().unwrap())
}
pub fn packet_size(&self) -> usize {
self.packet_size.load(Ordering::SeqCst)
}
pub fn messages(&self) -> Vec<String> {
self.messages.read().unwrap().clone()
}
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 {
messages.drain(max_length..);
}
*self.messages.write().unwrap() = messages;
}
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);
}
pub fn add_message(&self, max_length: usize, messages: Vec<String>) {
self.messages.write().unwrap().append(&mut messages.clone());
if self.messages.read().unwrap().len() > max_length {
self.messages.write().unwrap().drain(max_length..);
}
}
}
#[macro_export]
macro_rules! connect_rac {
($ctx:ident) => {
&mut connect(
&$ctx.config(|o| o.host.clone()),
$ctx.config(|o| o.ssl_enabled),
$ctx.config(|o| o.proxy.clone()),
$ctx.config(|o| o.wrac_enabled)
)?
};
}

911
src/chat/gui.rs Normal file
View File

@ -0,0 +1,911 @@
use std::sync::{mpsc::{channel, Receiver}, Arc, RwLock};
use std::cell::RefCell;
use std::time::{Duration, SystemTime};
use std::thread;
use chrono::Local;
use gtk4 as gtk;
use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader};
use gtk::prelude::*;
use gtk::gdk::{Cursor, Display, Texture};
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,
ControlFlow,
timeout_add_once
};
use gtk::pango::WrapMode;
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
};
use super::{config::{default_max_messages, default_update_time, get_config_path, save_config, Config},
ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize_message};
struct UiModel {
chat_box: GtkBox,
chat_scrolled: ScrolledWindow,
app: Application,
window: ApplicationWindow,
#[cfg(feature = "libnotify")]
notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
#[cfg(not(feature = "libnotify"))]
notifications: Arc<RwLock<Vec<String>>>
}
thread_local!(
static GLOBAL: RefCell<Option<(UiModel, Receiver<String>)>> = RefCell::new(None);
);
pub fn clear_chat_messages(ctx: Arc<Context>, message: String) {
let _ = ctx.sender.read().unwrap().clone().unwrap().send((message, true));
}
pub fn add_chat_message(ctx: Arc<Context>, message: String) {
let _ = ctx.sender.read().unwrap().clone().unwrap().send((message, false));
}
fn load_pixbuf(data: &[u8]) -> Pixbuf {
let loader = PixbufLoader::new();
loader.write(data).unwrap();
loader.close().unwrap();
loader.pixbuf().unwrap()
}
macro_rules! gui_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()))
.build();
hbox.append(&entry);
$vbox.append(&hbox);
entry
}
};
}
macro_rules! gui_usize_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.to_string()))
.build();
hbox.append(&entry);
$vbox.append(&hbox);
entry
}
};
}
macro_rules! gui_option_entry_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
let entry = Entry::builder()
.text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default())
.build();
hbox.append(&entry);
$vbox.append(&hbox);
entry
}
};
}
macro_rules! gui_checkbox_setting {
($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {
{
let hbox = GtkBox::new(Orientation::Horizontal, 5);
hbox.append(&Label::builder()
.label($e)
.build());
let entry = CheckButton::builder()
.active($ctx.config(|o| o.$i))
.build();
hbox.append(&entry);
$vbox.append(&hbox);
entry
}
};
}
fn open_settings(ctx: Arc<Context>, app: &Application) {
let vbox = GtkBox::new(Orientation::Vertical, 10);
vbox.set_margin_bottom(15);
vbox.set_margin_top(15);
vbox.set_margin_start(15);
vbox.set_margin_end(15);
let host_entry = gui_entry_setting!("Host", host, ctx, vbox);
let name_entry = gui_option_entry_setting!("Name", name, ctx, vbox);
let message_format_entry = gui_entry_setting!("Message Format", message_format, ctx, vbox);
let proxy_entry = gui_option_entry_setting!("Socks5 proxy", proxy, ctx, vbox);
let update_time_entry = gui_usize_entry_setting!("Update Time", update_time, ctx, vbox);
let max_messages_entry = gui_usize_entry_setting!("Max Messages", max_messages, ctx, vbox);
let hide_my_ip_entry = gui_checkbox_setting!("Hide My IP", hide_my_ip, ctx, vbox);
let show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, vbox);
let auth_enabled_entry = gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, vbox);
let ssl_enabled_entry = gui_checkbox_setting!("SSL Enabled", ssl_enabled, ctx, vbox);
let wrac_enabled_entry = gui_checkbox_setting!("WRAC Enabled", wrac_enabled, ctx, vbox);
let chunked_enabled_entry = gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, vbox);
let formatting_enabled_entry = gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, vbox);
let commands_enabled_entry = gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, vbox);
let notifications_enabled_entry = gui_checkbox_setting!("Notifications Enabled", notifications_enabled, ctx, vbox);
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] ssl_enabled_entry,
#[weak] chunked_enabled_entry,
#[weak] formatting_enabled_entry,
#[weak] commands_enabled_entry,
#[weak] notifications_enabled_entry,
#[weak] wrac_enabled_entry,
#[weak] proxy_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 {
Some(name)
}
},
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 {
let update_time = default_update_time();
update_time_entry.set_text(&update_time.to_string());
update_time
}
},
max_messages: {
let max_messages = max_messages_entry.text();
if let Ok(max_messages) = max_messages.parse::<usize>() {
max_messages
} else {
let max_messages = default_max_messages();
max_messages_entry.set_text(&max_messages.to_string());
max_messages
}
},
hide_my_ip: hide_my_ip_entry.is_active(),
show_other_ip: show_other_ip_entry.is_active(),
auth_enabled: auth_enabled_entry.is_active(),
ssl_enabled: ssl_enabled_entry.is_active(),
wrac_enabled: wrac_enabled_entry.is_active(),
chunked_enabled: chunked_enabled_entry.is_active(),
formatting_enabled: formatting_enabled_entry.is_active(),
commands_enabled: commands_enabled_entry.is_active(),
notifications_enabled: notifications_enabled_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);
}
));
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] ssl_enabled_entry,
#[weak] wrac_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);
save_config(get_config_path(), &config);
host_entry.set_text(&config.host);
name_entry.set_text(&config.name.unwrap_or_default());
proxy_entry.set_text(&config.proxy.unwrap_or_default());
message_format_entry.set_text(&config.message_format);
update_time_entry.set_text(&config.update_time.to_string());
max_messages_entry.set_text(&config.max_messages.to_string());
hide_my_ip_entry.set_active(config.hide_my_ip);
show_other_ip_entry.set_active(config.show_other_ip);
auth_enabled_entry.set_active(config.auth_enabled);
ssl_enabled_entry.set_active(config.ssl_enabled);
wrac_enabled_entry.set_active(config.wrac_enabled);
chunked_enabled_entry.set_active(config.chunked_enabled);
formatting_enabled_entry.set_active(config.formatting_enabled);
commands_enabled_entry.set_active(config.commands_enabled);
notifications_enabled_entry.set_active(config.notifications_enabled);
}
));
let window = Window::builder()
.application(app)
.title("Settings")
.default_width(400)
.default_height(500)
.decorated(true)
.child(&vbox)
.build();
let controller = gtk::EventControllerKey::new();
controller.connect_key_pressed({
let window = window.clone();
move |_, key, _, _| {
if key == gtk::gdk::Key::Escape {
window.close();
gtk::glib::Propagation::Proceed
} else {
gtk::glib::Propagation::Stop
}
}
});
window.add_controller(controller);
window.present();
}
fn build_menu(ctx: Arc<Context>, app: &Application) {
let menu = Menu::new();
let file_menu = Menu::new();
file_menu.append(Some("About"), Some("app.about"));
file_menu.append(Some("Close"), Some("app.close"));
let edit_menu = Menu::new();
edit_menu.append(Some("Settings"), Some("app.settings"));
menu.append_submenu(Some("File"), &file_menu);
menu.append_submenu(Some("Edit"), &edit_menu);
app.set_menubar(Some((&menu).into()));
app.add_action_entries([
ActionEntry::builder("settings")
.activate(clone!(
#[weak] ctx,
move |a: &Application, _, _| {
open_settings(ctx, a);
}
))
.build(),
ActionEntry::builder("close")
.activate(move |a: &Application, _, _| {
a.quit();
})
.build(),
ActionEntry::builder("about")
.activate(clone!(
#[weak] app,
move |_, _, _| {
AboutDialog::builder()
.application(&app)
.authors(["TheMixRay", "MeexReay"])
.license(" DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
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.")
.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"))))
.build()
.present();
}
))
.build()
]);
}
fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let main_box = GtkBox::new(Orientation::Vertical, 5);
main_box.set_css_classes(&["main-box"]);
let widget_box_overlay = Overlay::new();
let widget_box = GtkBox::new(Orientation::Horizontal, 5);
widget_box.set_css_classes(&["widget_box"]);
widget_box.append(&Calendar::builder()
.css_classes(["calendar"])
.show_heading(false)
.can_target(false)
.build());
let server_list_vbox = GtkBox::new(Orientation::Vertical, 5);
let server_list = ListBox::new();
for url in ["rac://meex.lol", "rac://meex.lol:11234", "rac://91.192.22.20"] {
let url = url.to_string();
let label = Label::builder()
.label(&url)
.halign(Align::Start)
.build();
let click = GestureClick::new();
click.connect_pressed(clone!(
#[weak] ctx,
move |_, _, _, _| {
let mut config = ctx.config.read().unwrap().clone();
config.host = url.clone();
ctx.set_config(&config);
save_config(get_config_path(), &config);
}
));
label.add_controller(click);
server_list.append(&label);
}
server_list_vbox.append(&Label::builder().label("Server List:").build());
server_list_vbox.append(&server_list);
widget_box.append(&server_list_vbox);
let fixed = Fixed::new();
fixed.set_can_target(false);
let konata = Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")));
konata.set_size_request(174, 127);
fixed.put(&konata, 325.0, 4.0);
let logo_gif = include_bytes!("images/logo.gif");
let logo = Picture::for_pixbuf(&load_pixbuf(logo_gif));
logo.set_size_request(152, 64);
let logo_anim = PixbufAnimation::from_stream(
&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();
let logo_anim = logo_anim.clone();
move || {
logo.set_pixbuf(Some(&logo_anim.pixbuf()));
logo_anim.advance(SystemTime::now());
ControlFlow::Continue
}
});
fixed.put(&logo, 262.0, 4.0);
let time = Label::builder()
.label(&Local::now().format("%H:%M").to_string())
.justify(Justification::Right)
.css_classes(["time"])
.build();
timeout_add_local(Duration::from_secs(1), {
let time = time.clone();
move || {
time.set_label(&Local::now().format("%H:%M").to_string());
ControlFlow::Continue
}
});
fixed.put(&time, 432.0, 4.0);
fixed.set_halign(Align::End);
widget_box_overlay.add_overlay(&fixed);
widget_box_overlay.set_child(Some(&widget_box));
main_box.append(&widget_box_overlay);
let chat_box = GtkBox::new(Orientation::Vertical, 2);
chat_box.set_css_classes(&["chat-box"]);
let chat_scrolled = ScrolledWindow::builder()
.child(&chat_box)
.vexpand(true)
.hexpand(true)
.margin_bottom(5)
.margin_end(5)
.margin_start(5)
.propagate_natural_height(true)
.build();
main_box.append(&chat_scrolled);
let send_box = GtkBox::new(Orientation::Horizontal, 5);
send_box.set_margin_bottom(5);
send_box.set_margin_end(5);
send_box.set_margin_start(5);
let text_entry = Entry::builder()
.placeholder_text("Message")
.css_classes(["send-button"])
.hexpand(true)
.build();
send_box.append(&text_entry);
let send_btn = Button::builder()
.label("Send")
.css_classes(["send-text"])
.cursor(&Cursor::from_name("pointer", None).unwrap())
.build();
send_btn.connect_clicked(clone!(
#[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 let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
let msg = format!("Send message error: {}", e.to_string()).to_string();
add_chat_message(ctx.clone(), msg);
}
}
));
text_entry.connect_activate(clone!(
#[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 let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
let msg = format!("Send message error: {}", e.to_string()).to_string();
add_chat_message(ctx.clone(), msg);
}
}
));
send_box.append(&send_btn);
main_box.append(&send_box);
let scrolled_window_weak = Downgrade::downgrade(&chat_scrolled);
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());
}
}
});
let window = ApplicationWindow::builder()
.application(app)
.title(format!("bRAC - Connected to {} as {}", ctx.config(|o| o.host.clone()), &ctx.name()))
.default_width(500)
.default_height(500)
.resizable(true)
.decorated(true)
.show_menubar(true)
.child(&main_box)
.build();
window.connect_default_width_notify({
let scrolled_window_weak = scrolled_window_weak.clone();
move |_| {
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());
}
});
}
});
window.present();
UiModel {
chat_scrolled,
chat_box,
app: app.clone(),
window: window.clone(),
#[cfg(feature = "libnotify")]
notifications: Arc::new(RwLock::new(Vec::<libnotify::Notification>::new())),
#[cfg(not(feature = "libnotify"))]
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
}
}
fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
let (sender, receiver) = channel();
*ctx.sender.write().unwrap() = Some(Arc::new(sender));
run_recv_loop(ctx.clone());
let (tx, rx) = channel();
#[cfg(feature = "libnotify")]
ui.window.connect_notify(Some("is-active"), move |a, _| {
if a.is_active() {
GLOBAL.with(|global| {
if let Some((ui, _)) = &*global.borrow() {
for i in ui.notifications.read().unwrap().clone() {
i.close().expect("libnotify close error");
}
}
});
}
});
#[cfg(not(feature = "libnotify"))]
ui.window.connect_notify(Some("is-active"), move |a, _| {
if a.is_active() {
GLOBAL.with(|global| {
if let Some((ui, _)) = &*global.borrow() {
for i in ui.notifications.read().unwrap().clone() {
ui.app.withdraw_notification(&i);
}
}
});
}
});
GLOBAL.with(|global| {
*global.borrow_mut() = Some((ui, rx));
});
thread::spawn({
let ctx = ctx.clone();
move || {
while let Ok((message, clear)) = receiver.recv() {
let _ = tx.send(message.clone());
let ctx = ctx.clone();
timeout_add_once(Duration::ZERO, move || {
GLOBAL.with(|global| {
if let Some((ui, rx)) = &*global.borrow() {
if clear {
while let Some(row) = ui.chat_box.last_child() {
ui.chat_box.remove(&row);
}
}
let message: String = rx.recv().unwrap();
on_add_message(ctx.clone(), &ui, message);
}
});
});
}
}
});
}
fn load_css() {
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()
} else {
false
};
let provider = CssProvider::new();
provider.load_from_data(&format!(
"{}\n{}",
if is_dark_theme {
include_str!("styles/dark.css")
} else {
include_str!("styles/light.css")
},
include_str!("styles/style.css")
));
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
#[cfg(feature = "libnotify")]
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
use libnotify::Notification;
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.close().unwrap();
notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap());
notification.show().expect("libnotify send error");
ui.notifications.write().unwrap().push(notification);
}
#[cfg(not(feature = "libnotify"))]
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
use std::{hash::{DefaultHasher, Hasher}, time::UNIX_EPOCH};
use gtk4::gio::Notification;
let mut hash = DefaultHasher::new();
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 notif = Notification::new(title);
notif.set_body(Some(&message));
ui.app.send_notification(Some(&id), &notif);
ui.notifications.write().unwrap().push(id);
}
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
let Some(message) = sanitize_message(message) else { return; };
if message.is_empty() {
return;
}
let hbox = GtkBox::new(Orientation::Horizontal, 2);
if let Some((date, ip, content, nick)) = parse_message(message.clone()) {
if let Some(ip) = ip {
if ctx.config(|o| o.show_other_ip) {
let ip_label = Label::builder()
.label(&ip)
.margin_end(10)
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-ip"])
.selectable(true)
.build();
hbox.append(&ip_label);
}
}
let date_label = Label::builder()
.label(format!("[{date}]"))
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-date"])
.selectable(true)
.build();
hbox.append(&date_label);
if let Some((name, color)) = nick {
let name_label = Label::builder()
.label(format!("<{name}>"))
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-name", &format!("message-name-{}", color)])
.selectable(true)
.build();
hbox.append(&name_label);
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, &format!("{}'s Message", &name), &content);
// let notif = Notification::new(&format!("{}'s Message", &name));
// notif.set_body(Some(&content));
// app.send_notification(Some("user-message"), &notif);
}
}
} else {
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, "System Message", &content);
// let notif = Notification::new("System Message");
// notif.set_body(Some(&content));
// app.send_notification(Some("system-message"), &notif);
}
}
}
let content_label = Label::builder()
.label(&content)
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-content"])
.selectable(true)
.wrap(true)
.wrap_mode(WrapMode::Char)
.build();
hbox.append(&content_label);
} else {
let content_label = Label::builder()
.label(&message)
.halign(Align::Start)
.valign(Align::Start)
.css_classes(["message-content"])
.selectable(true)
.wrap(true)
.wrap_mode(WrapMode::Char)
.build();
hbox.append(&content_label);
if !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(ctx.clone(), ui, "Chat Message", &message);
// let notif = Notification::new("Chat Message");
// notif.set_body(Some(&message));
// app.send_notification(Some("chat-message"), &notif);
}
}
}
ui.chat_box.append(&hbox);
timeout_add_local_once(Duration::from_millis(1000), move || {
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());
}
});
});
}
fn run_recv_loop(ctx: Arc<Context>) {
thread::spawn(move || {
loop {
if let Err(e) = recv_tick(ctx.clone()) {
let _ = print_message(ctx.clone(), format!("Print messages error: {}", e.to_string()).to_string());
thread::sleep(Duration::from_secs(1));
}
}
});
}
pub fn run_main_loop(ctx: Arc<Context>) {
#[cfg(feature = "libnotify")]
{
libnotify::init("ru.themixray.bRAC").expect("libnotify init error");
}
let application = Application::builder()
.application_id("ru.themixray.bRAC")
.flags(ApplicationFlags::FLAGS_NONE)
.build();
application.connect_activate({
let ctx = ctx.clone();
move |app| {
let ui = build_ui(ctx.clone(), app);
setup(app, ctx.clone(), ui);
load_css();
}
});
application.connect_startup({
let ctx = ctx.clone();
move |app| {
build_menu(ctx.clone(), app);
}
});
application.run_with_args::<&str>(&[]);
#[cfg(feature = "libnotify")]
{
libnotify::uninit();
}
}

BIN
src/chat/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
src/chat/images/konata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
src/chat/images/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,75 +0,0 @@
use std::sync::Arc;
use std::io::stdout;
use std::io::Write;
use colored::Colorize;
use super::{
super::{
config::Context,
proto::{connect, read_messages},
util::get_input
}, format_message, on_send_message
};
pub fn run_main_loop(ctx: Arc<Context>) {
loop {
match connect(&ctx.host, ctx.enable_ssl) {
Ok(mut stream) => {
match read_messages(
&mut stream,
ctx.max_messages,
ctx.messages.packet_size(),
!ctx.enable_ssl,
ctx.enable_chunked
) {
Ok(Some((messages, size))) => {
let messages: Vec<String> = if ctx.disable_formatting {
messages
} else {
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
};
if ctx.enable_chunked {
ctx.messages.append_and_store(ctx.max_messages, messages.clone(), size);
} else {
ctx.messages.update(ctx.max_messages, messages.clone(), size);
}
}
Err(e) => {
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
}
_ => {}
}
},
Err(e) => {
let msg = format!("Connect error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
}
}
let messages = ctx.messages.messages();
let mut out = stdout().lock();
write!(
out,
"{}\n{}\n{} ",
"\n".repeat(ctx.max_messages - messages.len()),
messages
.into_iter()
.map(|o| o.white().blink().to_string())
.collect::<Vec<String>>()
.join("\n"),
">".bright_yellow()
);
out.flush();
if let Some(message) = get_input("") {
if let Err(e) = on_send_message(ctx.clone(), &message) {
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
}
}
}
}

293
src/chat/mod.rs Normal file
View File

@ -0,0 +1,293 @@
use std::{
error::Error, sync::Arc, thread, time::{Duration, 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 gui::{add_chat_message, clear_chat_messages};
use lazy_static::lazy_static;
use regex::Regex;
use ctx::Context;
pub use gui::run_main_loop;
lazy_static! {
static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
pub static ref COLORED_USERNAMES: Vec<(Regex, String)> = vec![
(Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), "green".to_string()), // bRAC
(Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), "red".to_string()), // CRAB
(Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), "magenta".to_string()), // Mefidroniy
(Regex::new(r"<(.*?)> (.*)").unwrap(), "cyan".to_string()), // clRAC
];
}
pub mod gui;
pub mod config;
pub mod ctx;
const HELP_MESSAGE: &str = "Help message:
/help - show help message
/register password - register user
/login password - login user
/clear n - send empty message n times
/spam n text - send message with text n times
/ping - check server ping";
pub fn sanitize_text(input: &str) -> String {
let without_ansi = ANSI_REGEX.replace_all(input, "");
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
cleaned_text.into_owned()
}
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
for i in message.split("\n")
.map(|o| o.to_string()) {
print_message(ctx.clone(), i)?;
}
Ok(())
}
pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> {
let command = command.trim_start_matches("/");
let (command, args) = command.split_once(" ").unwrap_or((&command, ""));
let args = args.split(" ").collect::<Vec<&str>>();
if command == "clear" {
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 times = times.parse()?;
let msg = args[1..].join(" ");
for _ in 0..times {
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 {
add_message(ctx.clone(), "please provide password as the first argument")?;
return Ok(())
};
match register_user(connect_rac!(ctx), &ctx.name(), pass, !ctx.config(|o| o.ssl_enabled)) {
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))?
};
} else if command == "login" {
let Some(pass) = args.get(0) else {
add_message(ctx.clone(), "please provide password as the first argument")?;
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());
send_message(connect_rac!(ctx), &message)?;
let start = SystemTime::now();
loop {
let data = read_messages(
connect_rac!(ctx),
ctx.config(|o| o.max_messages),
before,
!ctx.config(|o| o.ssl_enabled),
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)) {
if last.contains(&message) {
break;
} else {
before = size;
}
} else {
before = size;
}
}
}
add_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?;
} else {
add_message(ctx.clone(), "Unknown command bruh")?;
}
Ok(())
}
pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String {
format!("{}{}{}",
if ctx.config(|o| o.hide_my_ip) {
"\r\x07"
} else {
""
},
message,
if !ctx.config(|o| o.hide_my_ip) {
let spaces = if ctx.config(|o| o.auth_enabled) {
39
} else {
54
};
if message.chars().count() < spaces {
" ".repeat(spaces-message.chars().count())
} else {
String::new()
}
} else {
String::new()
}
)
}
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
ctx.add_message(ctx.config(|o| o.max_messages), vec![message.clone()]);
add_chat_message(ctx.clone(), message);
Ok(())
}
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.ssl_enabled),
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[0].clone());
if messages.len() >= 2 {
for msg in &messages[1..] {
add_chat_message(ctx.clone(), msg.clone());
}
}
}
} else {
for msg in messages {
add_chat_message(ctx.clone(), msg.clone());
}
}
} else {
ctx.put_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
if messages.len() >= 1 {
clear_chat_messages(ctx.clone(), messages[0].clone());
if messages.len() >= 2 {
for msg in &messages[1..] {
add_chat_message(ctx.clone(), msg.clone());
}
}
}
}
},
Err(e) => {
println!("Read messages error: {}", e.to_string())
}
_ => {}
}
thread::sleep(Duration::from_millis(ctx.config(|o| o.update_time) as u64));
Ok(())
}
pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
if message.starts_with("/") && ctx.config(|o| o.commands_enabled) {
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)
);
if let Some(password) = ctx.registered.read().unwrap().clone() {
send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message, !ctx.config(|o| o.ssl_enabled))?;
} else if ctx.config(|o| o.auth_enabled) {
send_message_spoof_auth(connect_rac!(ctx), &message, !ctx.config(|o| o.ssl_enabled))?;
} else {
send_message(connect_rac!(ctx), &message)?;
}
}
Ok(())
}
pub fn sanitize_message(message: String) -> Option<String> {
let message = sanitize_text(&message);
let message = message.trim().to_string();
Some(message)
}
/// message -> (date, ip, text, (name, color))
pub fn parse_message(message: String) -> Option<(String, Option<String>, String, Option<(String, String)>)> {
if message.is_empty() {
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(),
);
let message = message
.trim_start_matches("(UNREGISTERED)")
.trim_start_matches("(UNAUTHORIZED)")
.trim_start_matches("(UNAUTHENTICATED)")
.trim()
.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())
} else {
(None, message)
};
let (message, nick) = match find_username_color(&message) {
Some((name, content, color)) => (content, Some((name, color))),
None => (message, None),
};
Some((date, ip, message, nick))
}
// message -> (nick, content, color)
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()))
}
}
None
}

View File

@ -1,386 +0,0 @@
use crossterm::{
cursor::{MoveLeft, MoveRight},
event::{self, Event, KeyCode, KeyModifiers, MouseEventKind},
execute,
terminal::{self, disable_raw_mode, enable_raw_mode}
};
use colored::Colorize;
use std::{
cmp::{max, min},
error::Error, io::{stdout, Write},
sync::{atomic::Ordering, Arc},
thread,
time::Duration
};
use super::{
super::{
config::Context,
proto::{connect, read_messages},
util::{char_index_to_byte_index, string_chunks}
}, format_message, on_send_message
};
pub fn print_console(ctx: Arc<Context>, messages: Vec<String>, input: &str) -> Result<(), Box<dyn Error>> {
let (width, height) = terminal::size()?;
let (width, height) = (width as usize, height as usize);
let mut messages = messages
.into_iter()
.flat_map(|o| string_chunks(&o, width as usize - 1))
.map(|o| (o.0.white().blink().to_string(), o.1))
.collect::<Vec<(String, usize)>>();
let messages_size = if messages.len() >= height {
messages.len()-height
} else {
for _ in 0..height-messages.len() {
messages.insert(0, (String::new(), 0));
}
0
};
let scroll = min(ctx.scroll.load(Ordering::SeqCst), messages_size);
let scroll_f = ((1f64 - scroll as f64 / (messages_size+1) as f64) * (height-2) as f64).round() as usize+1;
let messages = if height < messages.len() {
if scroll < messages.len() - height {
messages[
messages.len()-height-scroll..
messages.len()-scroll
].to_vec()
} else {
if scroll < messages.len() {
messages[
0..
messages.len()-scroll
].to_vec()
} else {
vec![]
}
}
} else {
messages
};
let formatted_messages = if ctx.disable_formatting {
messages
.into_iter()
.map(|(i, _)| i)
.collect::<Vec<String>>()
} else {
messages
.into_iter()
.enumerate()
.map(|(i, (s, l))| {
format!("{}{}{}",
s,
" ".repeat(width - 1 - l),
if i == scroll_f {
"".bright_yellow()
} else {
"".yellow()
}
)
})
.collect::<Vec<String>>()
};
let text = format!(
"{}\r\n{} {}",
formatted_messages.join("\r\n"),
">".bright_yellow(),
input
);
let mut out = stdout().lock();
write!(out, "{}", text)?;
out.flush()?;
Ok(())
}
fn replace_input(cursor: usize, len: usize, text: &str) {
let spaces = if text.chars().count() < len {
len-text.chars().count()
} else {
0
};
write!(stdout(),
"{}{}{}{}",
MoveLeft(1).to_string().repeat(cursor),
text,
" ".repeat(spaces),
MoveLeft(1).to_string().repeat(spaces)
).unwrap();
stdout().lock().flush().unwrap();
}
fn replace_input_left(cursor: usize, len: usize, text: &str, left: usize) {
let spaces = if text.chars().count() < len {
len-text.chars().count()
} else {
0
};
write!(stdout(),
"{}{}{}{}",
MoveLeft(1).to_string().repeat(cursor),
text,
" ".repeat(spaces),
MoveLeft(1).to_string().repeat(len-left)
).unwrap();
stdout().lock().flush().unwrap();
}
fn poll_events(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
let mut history: Vec<String> = vec![String::new()];
let mut history_cursor: usize = 0;
let mut cursor: usize = 0;
let input = ctx.input.clone();
let messages = ctx.messages.clone();
loop {
if !event::poll(Duration::from_millis(50)).unwrap_or(false) { continue }
let event = match event::read() {
Ok(i) => i,
Err(_) => { continue },
};
match event {
Event::Key(event) => {
match event.code {
KeyCode::Enter => {
let message = input.read().unwrap().clone();
if !message.is_empty() {
replace_input(cursor, message.chars().count(), "");
input.write().unwrap().clear();
cursor = 0;
history.push(String::new());
history_cursor = history.len()-1;
if let Err(e) = on_send_message(ctx.clone(), &message) {
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?;
}
} else {
print_console(
ctx.clone(),
messages.messages(),
""
)?;
}
}
KeyCode::Backspace => {
if cursor == 0 || !(0..=history[history_cursor].len()).contains(&(cursor)) {
continue
}
let len = input.read().unwrap().chars().count();
let i = char_index_to_byte_index(&history[history_cursor], cursor-1);
history[history_cursor].remove(i);
*input.write().unwrap() = history[history_cursor].clone();
replace_input_left(cursor, len, &history[history_cursor], cursor-1);
cursor -= 1;
}
KeyCode::Delete => {
if cursor == 0 || !(0..history[history_cursor].len()).contains(&(cursor)) {
continue
}
let len = input.read().unwrap().chars().count();
let i = char_index_to_byte_index(&history[history_cursor], cursor);
history[history_cursor].remove(i);
*input.write().unwrap() = history[history_cursor].clone();
replace_input_left(cursor, len, &history[history_cursor], cursor);
}
KeyCode::Esc => {
on_close();
break;
}
KeyCode::Up | KeyCode::Down => {
history_cursor = if event.code == KeyCode::Up {
max(history_cursor, 1) - 1
} else {
min(history_cursor + 1, history.len() - 1)
};
let len = input.read().unwrap().chars().count();
*input.write().unwrap() = history[history_cursor].clone();
replace_input(cursor, len, &history[history_cursor]);
cursor = history[history_cursor].chars().count();
}
KeyCode::PageUp => {
let height = terminal::size().unwrap().1 as usize;
ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+height, ctx.messages.messages().len()), Ordering::SeqCst);
print_console(
ctx.clone(),
messages.messages(),
&input.read().unwrap()
)?;
}
KeyCode::PageDown => {
let height = terminal::size().unwrap().1 as usize;
ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), height)-height, Ordering::SeqCst);
print_console(
ctx.clone(),
messages.messages(),
&input.read().unwrap()
)?;
}
KeyCode::Left => {
if cursor > 0 {
cursor -= 1;
write!(stdout(), "{}", MoveLeft(1).to_string(), ).unwrap();
stdout().lock().flush().unwrap();
}
}
KeyCode::Right => {
if cursor < history[history_cursor].len() {
cursor += 1;
write!(stdout(), "{}", MoveRight(1).to_string(), ).unwrap();
stdout().lock().flush().unwrap();
}
}
KeyCode::Char(c) => {
if event.modifiers.contains(KeyModifiers::CONTROL) && "zxcZXCячсЯЧС".contains(c) {
on_close();
break;
}
let i = char_index_to_byte_index(&history[history_cursor], cursor);
history[history_cursor].insert(i, c);
input.write().unwrap().insert(i, c);
write!(stdout(), "{}{}",
history[history_cursor][i..].to_string(),
MoveLeft(1).to_string().repeat(history[history_cursor].chars().count()-cursor-1)
).unwrap();
stdout().lock().flush().unwrap();
cursor += 1;
}
_ => {}
}
},
Event::Paste(data) => {
let i = char_index_to_byte_index(&history[history_cursor], cursor);
history[history_cursor].insert_str(i, &data);
input.write().unwrap().insert_str(i, &data);
write!(stdout(), "{}{}",
history[history_cursor][cursor..].to_string(),
MoveLeft(1).to_string().repeat(history[history_cursor].len()-cursor-1)
).unwrap();
stdout().lock().flush().unwrap();
cursor += data.len();
},
Event::Resize(_, _) => {
print_console(
ctx.clone(),
messages.messages(),
&input.read().unwrap()
)?;
},
Event::Mouse(data) => {
match data.kind {
MouseEventKind::ScrollUp => {
ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+3, ctx.messages.messages().len()), Ordering::SeqCst);
print_console(
ctx.clone(),
messages.messages(),
&input.read().unwrap()
)?;
},
MouseEventKind::ScrollDown => {
ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), 3)-3, Ordering::SeqCst);
print_console(
ctx.clone(),
messages.messages(),
&input.read().unwrap()
)?;
},
_ => {}
}
}
_ => {}
}
}
Ok(())
}
pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
match read_messages(
&mut connect(&ctx.host, ctx.enable_ssl)?,
ctx.max_messages,
ctx.messages.packet_size(),
!ctx.enable_ssl,
ctx.enable_chunked
) {
Ok(Some((messages, size))) => {
let messages: Vec<String> = if ctx.disable_formatting {
messages
} else {
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
};
if ctx.enable_chunked {
ctx.messages.append_and_store(ctx.max_messages, messages.clone(), size);
print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?;
} else {
ctx.messages.update(ctx.max_messages, messages.clone(), size);
print_console(ctx.clone(), messages, &ctx.input.read().unwrap())?;
}
},
Err(e) => {
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?;
}
_ => {}
}
thread::sleep(Duration::from_millis(ctx.update_time as u64));
Ok(())
}
pub fn on_close() {
disable_raw_mode().unwrap();
execute!(stdout(), event::DisableMouseCapture).unwrap();
}
pub fn run_main_loop(ctx: Arc<Context>) {
enable_raw_mode().unwrap();
execute!(stdout(), event::EnableMouseCapture).unwrap();
if let Err(e) = print_console(ctx.clone(), Vec::new(), &ctx.input.read().unwrap()) {
let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
let _ = print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap());
}
thread::spawn({
let ctx = ctx.clone();
move || {
loop {
if let Err(e) = recv_tick(ctx.clone()) {
let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
let _ = print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap());
thread::sleep(Duration::from_secs(1));
}
}
}
});
if let Err(e) = poll_events(ctx.clone()) {
let msg = format!("Poll events error: {}", e.to_string()).bright_red().to_string();
ctx.messages.append(ctx.max_messages, vec![msg]);
let _ = print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap());
}
}

3
src/chat/styles/dark.css Normal file
View File

@ -0,0 +1,3 @@
.message-content { color:rgb(255, 255, 255); }
.message-date { color:rgb(146, 146, 146); }
.message-ip { color:rgb(73, 73, 73); }

View File

@ -0,0 +1,3 @@
.message-content { color:rgb(0, 0, 0); }
.message-date { color:rgb(41, 41, 41); }
.message-ip { color:rgb(88, 88, 88); }

24
src/chat/styles/style.css Normal file
View File

@ -0,0 +1,24 @@
.send-button, .send-text { border-radius: 0; }
.calendar {
transform: scale(0.6);
margin: -35px;
}
.widget_box {
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
min-height: 121px;
}
.time {
font-size: 20px;
font-family: monospace;
font-weight: bold;
}
.message-name { font-weight: bold; }
.message-name-green { color: #70fa7a; }
.message-name-red { color: #fa7070; }
.message-name-magenta { color: #da70fa; }
.message-name-cyan { color: #70fadc; }

View File

@ -1,249 +0,0 @@
use std::{str::FromStr, sync::{atomic::AtomicUsize, Arc, RwLock}};
#[allow(unused_imports)]
use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration};
use colored::Colorize;
use rand::random;
use serde_yml;
use clap::Parser;
use crate::chat::ChatStorage;
use super::util::get_input;
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
#[derive(serde::Serialize, serde::Deserialize)]
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_max_messages")]
pub max_messages: usize,
#[serde(default)]
pub enable_ip_viewing: bool,
#[serde(default)]
pub disable_ip_hiding: bool,
#[serde(default)]
pub enable_auth: bool,
#[serde(default)]
pub enable_ssl: bool,
#[serde(default)]
pub enable_chunked: bool,
}
fn default_max_messages() -> usize { 200 }
fn default_update_time() -> usize { 50 }
fn default_host() -> String { "meex.lol:11234".to_string() }
fn default_message_format() -> String { MESSAGE_FORMAT.to_string() }
fn ask_usize(name: impl ToString, default: usize) -> usize {
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
.and_then(|o| o.parse().ok()).unwrap_or(default)
}
fn ask_string(name: impl ToString, default: impl ToString + Clone) -> String {
ask_string_option(name, default.clone()).unwrap_or(default.to_string())
}
fn ask_string_option(name: impl ToString, default: impl ToString) -> Option<String> {
let default = default.to_string();
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
}
fn ask_bool(name: impl ToString, default: bool) -> bool {
get_input(format!("{} (Y/N, default: {}) {} ", name.to_string().bold(), if default { "Y" } else { "N" }, ">".bold()).bright_yellow())
.map(|o| o.to_lowercase() != "n")
.unwrap_or(default)
}
pub fn configure(path: PathBuf) -> Config {
println!("{}", "To configure the client, please answer a few questions. It won't take long.".yellow());
println!("{}", "You can reconfigure client in any moment via `bRAC --configure`".yellow());
println!("{}", format!("Config stores in path `{}`", path.to_string_lossy()).yellow());
println!();
let host = ask_string("Host", default_host());
let name = ask_string_option("Name", "ask every time");
let update_time = ask_usize("Update interval", default_update_time());
let max_messages = ask_usize("Max messages", default_max_messages());
let message_format = ask_string("Message format", default_message_format());
let enable_ip_viewing = ask_bool("Enable users IP viewing?", true);
let disable_ip_hiding = ask_bool("Enable your IP viewing?", false);
let enable_auth = ask_bool("Enable auth-mode?", false);
let enable_ssl = ask_bool("Enable SSL?", false);
let enable_chunked = ask_bool("Enable chunked reading?", true);
let config = Config {
host,
name,
message_format,
update_time,
max_messages,
enable_ip_viewing,
disable_ip_hiding,
enable_auth,
enable_ssl,
enable_chunked
};
let config_text = serde_yml::to_string(&config).expect("Config save error");
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
fs::write(&path, config_text).expect("Config save error");
println!();
println!("{}", "Config saved! You can reconfigure it in any moment via `bRAC --configure`".yellow());
config
}
pub fn load_config(path: PathBuf) -> Config {
if !fs::exists(&path).unwrap_or_default() {
let config = configure(path.clone());
thread::sleep(Duration::from_secs(4));
config
} else {
let config = &fs::read_to_string(&path).expect("Config load error");
serde_yml::from_str(config).expect("Config load error")
}
}
pub fn get_config_path() -> PathBuf {
let mut config_dir = PathBuf::from_str(".").unwrap();
#[cfg(feature = "homedir")]
if let Some(dir) = {
let home_dir = {
use homedir::my_home;
my_home().ok().flatten()
};
#[cfg(target_os = "linux")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
#[cfg(target_os = "macos")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
#[cfg(target_os = "windows")]
let config_dir = {
let appdata = env::var("APPDATA").map(|o| o.join("bRAC"));
Path::new(&appdata)
};
config_dir
} {
config_dir = dir;
}
config_dir.join("config.yml")
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
/// Print config path
#[arg(short='p', long)]
pub config_path: bool,
/// Use specified host
#[arg(short='H', long)]
pub host: Option<String>,
/// Use specified name
#[arg(short='n', long)]
pub name: Option<String>,
/// Use specified message format
#[arg(short='F', long)]
pub message_format: Option<String>,
/// Print unformatted messages from chat and exit
#[arg(short='r', long)]
pub read_messages: bool,
/// Send unformatted message to chat and exit
#[arg(short='s', long, value_name="MESSAGE")]
pub send_message: Option<String>,
/// Disable message formatting and sanitizing
#[arg(short='f', long)]
pub disable_formatting: bool,
/// Disable slash commands
#[arg(short='c', long)]
pub disable_commands: bool,
/// Disable ip hiding
#[arg(short='i', long)]
pub disable_ip_hiding: bool,
/// Enable users IP viewing
#[arg(short='v', long)]
pub enable_users_ip_viewing: bool,
/// Configure client
#[arg(short='C', long)]
pub configure: bool,
/// Enable authentication
#[arg(short='a', long)]
pub enable_auth: bool,
/// Enable SSL
#[arg(short='S', long)]
pub enable_ssl: bool,
/// Enable chunked reading
#[arg(short='u', long)]
pub enable_chunked: bool,
}
pub struct Context {
pub messages: Arc<ChatStorage>,
pub input: Arc<RwLock<String>>,
pub host: String,
pub name: String,
pub disable_formatting: bool,
pub disable_commands: bool,
pub disable_hiding_ip: bool,
pub message_format: String,
pub update_time: usize,
pub max_messages: usize,
pub enable_ip_viewing: bool,
pub scroll: Arc<AtomicUsize>,
pub enable_auth: bool,
pub enable_ssl: bool,
pub enable_chunked: bool,
}
impl Context {
pub fn new(config: &Config, args: &Args) -> Context {
Context {
messages: Arc::new(ChatStorage::new()),
input: Arc::new(RwLock::new(String::new())),
message_format: args.message_format.clone().unwrap_or(config.message_format.clone()),
host: args.host.clone().unwrap_or(config.host.clone()),
name: args.name.clone().or(config.name.clone()).unwrap_or_else(|| ask_string("Name", format!("Anon#{:X}", random::<u16>()))),
disable_formatting: args.disable_formatting,
disable_commands: args.disable_commands,
disable_hiding_ip: args.disable_ip_hiding,
update_time: config.update_time,
max_messages: config.max_messages,
enable_ip_viewing: args.enable_users_ip_viewing || config.enable_ip_viewing,
scroll: Arc::new(AtomicUsize::new(0)),
enable_auth: args.enable_auth || config.enable_auth,
enable_ssl: args.enable_ssl || config.enable_ssl,
enable_chunked: args.enable_chunked || config.enable_chunked,
}
}
}

View File

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

View File

@ -1,12 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use clap::Parser;
use bRAC::config::{configure, get_config_path, load_config, Args, Context};
use bRAC::proto::{connect, read_messages, send_message}; use bRAC::proto::{connect, read_messages, send_message};
use bRAC::chat::run_main_loop; use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
use clap::Parser;
fn main() { fn main() {
#[cfg(feature = "winapi")]
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();
@ -16,22 +17,16 @@ fn main() {
return; return;
} }
if args.configure { let mut config = load_config(config_path);
configure(config_path);
return;
}
let config = load_config(config_path);
let ctx = Arc::new(Context::new(&config, &args));
if args.read_messages { if args.read_messages {
let mut stream = connect(&ctx.host, ctx.enable_ssl).expect("Error reading message"); let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message");
print!("{}", read_messages( print!("{}", read_messages(
&mut stream, &mut stream,
ctx.max_messages, config.max_messages,
0, 0,
!ctx.enable_ssl, !config.ssl_enabled,
false false
) )
.ok().flatten() .ok().flatten()
@ -40,12 +35,21 @@ fn main() {
} }
if let Some(message) = &args.send_message { if let Some(message) = &args.send_message {
send_message(&mut connect(&ctx.host, ctx.enable_ssl).expect("Error sending message"), message).expect("Error sending message"); let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error sending message");
send_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));
run_main_loop(ctx.clone()); run_main_loop(ctx.clone());
} }

View File

@ -1,183 +0,0 @@
#![allow(unused)]
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::TcpStream};
pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {}
impl<T: Read + Write + Unpin + Send + Sync + Debug> RacStream for T {}
/// Create RAC connection (also you can just TcpStream::connect)
///
/// host - host string, example: "example.com:12345", "example.com" (default port is 42666)
/// ssl - wrap with ssl client, write false if you dont know what it is
pub fn connect(host: &str, ssl: bool) -> Result<Box<dyn RacStream>, Box<dyn Error>> {
let host = if host.contains(":") {
host.to_string()
} else {
format!("{host}:42666")
};
#[cfg(feature = "ssl")]
{
use native_tls::TlsConnector;
if ssl {
let ip: String = host.split_once(":")
.map(|o| o.0.to_string())
.unwrap_or(host.clone());
return Ok(Box::new(TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()?
.connect(&ip, connect(&host, false)?)?))
}
}
Ok(Box::new(TcpStream::connect(host)?))
}
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box<dyn Error>> {
stream.write_all(format!("\x01{message}").as_bytes())?;
Ok(())
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
///
/// returns whether the user was registered
pub fn register_user(stream: &mut (impl Write + Read), name: &str, password: &str) -> Result<bool, Box<dyn Error>> {
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
let mut buf = vec![0];
if let Ok(1) = stream.read(&mut buf) {
Ok(buf[0] == 0)
} else {
Ok(true)
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
///
/// returns 0 if the message was sent successfully
/// 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, message: &str) -> Result<u8, Box<dyn Error>> {
stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?;
let mut buf = vec![0];
if let Ok(1) = stream.read(&mut buf) {
Ok(buf[0])
} else {
Ok(0)
}
}
/// Send message with fake auth
///
/// Explaination:
///
/// let (name, message) = message.split("> ") else { return send_message(stream, message) }
/// if send_message_auth(name, name, message) != 0 {
/// let name = "\x1f" + "name"
/// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message)
/// }
pub fn send_message_spoof_auth(stream: &mut (impl Write + Read), message: &str) -> Result<(), Box<dyn Error>> {
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
stream.write_all(format!("\x02{name}\n{name}\n{message}").as_bytes())?;
let mut buf = vec![0; 1];
if let Ok(_) = stream.read_exact(&mut buf) {
let name = format!("\x1f{name}");
register_user(stream, &name, &name)?;
let message = format!("{name}> {message}");
send_message_spoof_auth(stream, &message)
} else {
Ok(())
}
}
/// Skip null bytes and return first non-null byte
pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
loop {
let mut buf = vec![0; 1];
stream.read_exact(&mut buf)?;
if buf[0] != 0 {
break Ok(buf)
}
}
}
pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
while vec.ends_with(&[0]) {
vec.remove(vec.len()-1);
}
Ok(())
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// start_null - start with skipping null bytes
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut (impl Read + Write),
max_messages: usize,
last_size: usize,
start_null: bool,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write_all(&[0x00])?;
let packet_size = {
let mut data = vec![0; 10];
let len = stream.read(&mut data)?;
data.truncate(len);
String::from_utf8(data)?
.trim_matches(char::from(0))
.parse()?
};
if last_size == packet_size {
return Ok(None);
}
let to_read = if !chunked || last_size == 0 {
stream.write_all(&[0x01])?;
packet_size
} else {
stream.write_all(format!("\x02{}", last_size).as_bytes())?;
packet_size - last_size
};
let packet_data = {
let mut data = vec![0; to_read];
stream.read_exact(&mut data)?;
String::from_utf8_lossy(&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 })
.map(|o| o.to_string())
.collect();
Ok(Some((lines, packet_size)))
}

267
src/proto/mod.rs Normal file
View File

@ -0,0 +1,267 @@
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, time::Duration};
use native_tls::{TlsConnector, TlsStream};
use socks::Socks5Stream;
use tungstenite::WebSocket;
pub mod rac;
pub mod wrac;
pub trait Stream: Read + Write + Unpin + Send + Sync + Debug {
fn set_read_timeout(&self, timeout: Duration);
fn set_write_timeout(&self, timeout: Duration);
}
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)); }
}
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)); }
}
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); }
}
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); }
}
pub enum RacStream {
WRAC(WebSocket<Box<dyn Stream>>),
RAC(Box<dyn Stream>)
}
/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \
/// `socks5://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `https://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \
/// `user:pass@127.0.0.1:12345 -> ("127.0.0.1:12345", ("user", "pass"))`
pub fn parse_socks5_url(url: &str) -> Option<(String, Option<(String, String)>)> {
let (_, url) = url.split_once("://").unwrap_or(("", url));
let (url, _) = url.split_once("/").unwrap_or((url, ""));
if let Some((auth, url)) = url.split_once("@") {
let (user, pass) = auth.split_once(":")?;
Some((url.to_string(), Some((user.to_string(), pass.to_string()))))
} else {
Some((url.to_string(), None))
}
}
/// url -> (host, ssl, wrac) \
/// `127.0.0.1` -> `("127.0.0.1:42666", false, false)` \
/// `127.0.0.1:12345` -> `("127.0.0.1:12345", false, false)` \
/// `rac://127.0.0.1/` -> `("127.0.0.1:42666", false, false)` \
/// `racs://127.0.0.1/` -> `("127.0.0.1:42667", true, false)` \
/// `wrac://127.0.0.1/` -> `("127.0.0.1:52666", false, true)` \
/// `wracs://127.0.0.1/` -> `(127.0.0.1:52667, true, true)` \
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
))
},
_ => None,
}
}
/// Create RAC connection (also you can just TcpStream::connect)
///
/// host - host string, example: "example.com:12345", "example.com" (default port is 42666)
/// ssl - wrap with ssl client, write false if you dont know what it is
/// proxy - socks5 proxy (host, (user, pass))
/// wrac - to use wrac protocol
pub fn connect(host: &str, ssl: bool, proxy: Option<String>, wrac: bool) -> Result<RacStream, Box<dyn Error>> {
let (host, ssl_, wrac_) = parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?;
let (ssl, wrac) = (ssl_ || ssl, wrac_ || wrac);
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)?)
} else {
Box::new(Socks5Stream::connect(&proxy, host.as_str())?)
}
} else {
return Err("proxy parse error".into());
}
} else {
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(":")
.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)?)
} else {
stream
};
stream.set_read_timeout(Duration::from_secs(3));
stream.set_write_timeout(Duration::from_secs(3));
if wrac {
let (client, _) = tungstenite::client(
&format!("ws{}://{host}", if ssl { "s" } else { "" }),
stream
)?;
Ok(RacStream::WRAC(client))
} else {
Ok(RacStream::RAC(stream))
}
}
/// Send message with fake auth
///
/// Explaination:
///
/// let (name, message) = message.split("> ") else { return send_message(stream, message) }
/// if send_message_auth(name, name, message) != 0 {
/// let name = "\x1f" + name
/// register_user(stream, name, name)
/// send_message_spoof_auth(stream, name + "> " + message)
/// }
pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str, remove_null: bool) -> 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, &message, &message, remove_null) {
if f != 0 {
let name = format!("\x1f{name}");
register_user(stream, &name, &name, remove_null)?;
send_message_spoof_auth(stream, &format!("{name}> {message}"), remove_null)?;
}
}
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>> {
match stream {
RacStream::WRAC(websocket) => wrac::send_message(websocket, message),
RacStream::RAC(stream) => rac::send_message(stream, message)
}
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns whether the user was registered
pub fn register_user(
stream: &mut RacStream,
name: &str,
password: &str,
remove_null: bool
) -> 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, remove_null)
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns 0 if the message was sent successfully
/// 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,
remove_null: bool
) -> 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, remove_null)
}
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// remove_null - start with skipping null bytes
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut RacStream,
max_messages: usize,
last_size: usize,
remove_null: 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, remove_null, chunked)
}
}

169
src/proto/rac.rs Normal file
View File

@ -0,0 +1,169 @@
use std::{error::Error, io::{Read, Write}};
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box<dyn Error>> {
stream.write_all(format!("\x01{message}").as_bytes())?;
Ok(())
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns whether the user was registered
pub fn register_user(
stream: &mut (impl Write + Read),
name: &str,
password: &str,
remove_null: bool
) -> Result<bool, Box<dyn Error>> {
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
if remove_null {
if let Ok(out) = skip_null(stream) {
Ok(out[0] == 0)
} else {
Ok(true)
}
} else {
let mut buf = vec![0];
if let Ok(1) = stream.read(&mut buf) {
Ok(buf[0] == 0)
} else {
Ok(true)
}
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
/// remove_null - remove null bytes on reading
///
/// returns 0 if the message was sent successfully
/// 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,
message: &str,
remove_null: bool
) -> Result<u8, Box<dyn Error>> {
stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?;
if remove_null {
if let Ok(out) = skip_null(stream) {
Ok(out[0])
} else {
Ok(0)
}
} else {
let mut buf = vec![0];
if let Ok(1) = stream.read(&mut buf) {
Ok(buf[0])
} else {
Ok(0)
}
}
}
/// Skip null bytes and return first non-null byte
pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
loop {
let mut buf = vec![0; 1];
stream.read_exact(&mut buf)?;
if buf[0] != 0 {
break Ok(buf)
}
}
}
/// 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);
}
Ok(())
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// remove_null - start with skipping null bytes
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
stream: &mut (impl Read + Write),
max_messages: usize,
last_size: usize,
remove_null: bool,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write_all(&[0x00])?;
let packet_size = {
let data = if remove_null {
let mut data = skip_null(stream)?;
let mut buf = vec![0; 10];
let len = stream.read(&mut buf)?;
buf.truncate(len);
data.append(&mut buf);
remove_trailing_null(&mut data)?;
data
} else {
let mut data = vec![0; 10];
let len = stream.read(&mut data)?;
data.truncate(len);
data
};
String::from_utf8(data)?
.trim_matches(char::from(0))
.parse()?
};
if last_size == packet_size {
return Ok(None);
}
let to_read = if !chunked || last_size == 0 {
stream.write_all(&[0x01])?;
packet_size
} else {
stream.write_all(format!("\x02{}", last_size).as_bytes())?;
packet_size - last_size
};
let packet_data = if remove_null {
let mut data = skip_null(stream)?;
let mut buf = vec![0; to_read - 1];
stream.read_exact(&mut buf)?;
data.append(&mut buf);
data
} else {
let mut data = vec![0; to_read];
stream.read_exact(&mut data)?;
data
};
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 })
.map(|o| o.to_string())
.collect();
Ok(Some((lines, packet_size)))
}

128
src/proto/wrac.rs Normal file
View File

@ -0,0 +1,128 @@
use std::{error::Error, io::{Read, Write}};
use tungstenite::{WebSocket, Message};
/// Send message
///
/// stream - any stream that can be written to
/// message - message text
pub fn send_message(
stream: &mut WebSocket<impl Write + Read>,
message: &str
) -> Result<(), Box<dyn Error>> {
stream.write(Message::Binary(format!("\x01{message}").as_bytes().to_vec().into()))?;
stream.flush()?;
Ok(())
}
/// Register user
///
/// stream - any stream that can be written to
/// name - user name
/// password - user password
///
/// returns whether the user was registered
pub fn register_user(
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.flush()?;
if let Ok(msg) = stream.read() {
Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0)
} else {
Ok(true)
}
}
/// Send message with auth
///
/// stream - any stream that can be written to
/// message - message text
/// name - user name
/// password - user password
///
/// returns 0 if the message was sent successfully
/// 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
) -> Result<u8, Box<dyn Error>> {
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() {
Ok(0)
} else {
Ok(*msg.into_data().get(0).unwrap_or(&0))
}
} else {
Ok(0)
}
}
/// Read messages
///
/// max_messages - max messages in list
/// last_size - last returned packet size
/// chunked - is chunked reading enabled
///
/// returns (messages, packet size)
pub fn read_messages(
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()?;
let packet_size = {
let msg = stream.read()?;
if !msg.is_binary() {
return Err("msg is not binary".into());
}
let len = msg.into_data().to_vec();
String::from_utf8(len)?
.trim_matches(char::from(0))
.parse()?
};
if last_size == packet_size {
return Ok(None);
}
let to_read = if !chunked || last_size == 0 {
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()))?;
packet_size - last_size
};
stream.flush()?;
let msg = stream.read()?;
if !msg.is_binary() {
return Err("msg is not binary".into());
}
let packet_data = msg.into_data().to_vec();
if packet_data.len() > to_read {
return Err("too big msg".into());
}
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 })
.map(|o| o.to_string())
.collect();
Ok(Some((lines, packet_size)))
}

View File

@ -1,83 +0,0 @@
use std::{io::{stdin, stdout, BufRead, Write}, ops::Range};
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
}
fn get_matches(regex: &Regex, text: &str) -> Vec<Range<usize>> {
regex.find_iter(text).map(|mat| mat.range()).collect()
}
pub fn char_index_to_byte_index(text: &str, char_index: usize) -> usize {
text.char_indices().skip(char_index).next().map(|o| o.0).unwrap_or(text.len())
}
pub fn string_chunks(text: &str, width: usize) -> Vec<(String, usize)> {
let mut norm: Vec<bool> = vec![true; text.chars().count()];
for range in get_matches(&ANSI_REGEX, text) {
for i in range {
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
norm[index] = false;
}
}
}
for range in get_matches(&CONTROL_CHARS_REGEX, text) {
for i in range {
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
norm[index] = false;
}
}
}
let mut now_chunk = String::new();
let mut chunks = Vec::new();
let mut length = 0;
for (i, b) in norm.iter().enumerate() {
if *b {
length += 1;
}
now_chunk.push(text.chars().skip(i).next().unwrap());
if length == width {
chunks.push((now_chunk.clone(), length));
now_chunk.clear();
length = 0;
}
}
if !now_chunk.is_empty() {
chunks.push((now_chunk.clone(), length));
}
chunks
}
pub fn sanitize_text(input: &str) -> String {
let without_ansi = ANSI_REGEX.replace_all(input, "");
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
cleaned_text.into_owned()
}
pub fn get_input(prompt: impl ToString) -> Option<String> {
let prompt = prompt.to_string();
if !prompt.is_empty() {
let mut out = stdout().lock();
out.write_all(prompt.as_bytes()).ok()?;
out.flush().ok()?;
}
let input = stdin().lock().lines().next()
.map(|o| o.ok())
.flatten()?;
if input.is_empty() {
None
} else {
Some(input.to_string())
}
}

35
uninstall.bat Normal file
View File

@ -0,0 +1,35 @@
@echo off
net session >nul 2>&1 || (
echo This script requires administrator privileges.
pause
exit /b
)
set "TARGET=C:\Program Files\bRAC\bRAC.exe"
for /d %%u in ("C:\Users\*") do (
call :d "%%u\AppData\Roaming\Microsoft\Windows\Desktop"
call :d "%%u\Desktop"
)
cd /d "%TEMP%"
rmdir /s /q "C:\Program Files\bRAC"
exit /b
:d
if not exist "%~1" exit /b
for %%f in ("%~1\*.lnk") do (
call :c "%%~f"
)
exit /b
:c
set "v=%TEMP%\_c.vbs"
> "%v%" echo Set o=CreateObject("WScript.Shell")
>>"%v%" echo Set l=o.CreateShortcut("%~1")
>>"%v%" echo WScript.Echo l.TargetPath
for /f "usebackq delims=" %%t in (`wscript //nologo "%v%"`) do (
if /I "%%t"=="%TARGET%" del /f /q "%~1"
)
del "%v%" >nul
exit /b

14
uninstall.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
getent passwd | while IFS=: read -r name password uid gid gecos home shell; do
rm -rf $home/.config/bRAC;
done
rm -f /bin/bRAC
rm -f ru.themixray.bRAC.png /usr/share/pixmaps
rm -f ru.themixray.bRAC.desktop /usr/share/applications