mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-05-06 21:48:03 +03:00
Compare commits
89 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa75ca60c4 | |||
091c1bca03 | |||
73f7c565e1 | |||
06c27aac63 | |||
2a3473853b | |||
e55d5a7a55 | |||
cb17464d8f | |||
015d8fca59 | |||
cc01dd1e49 | |||
cdbe45254d | |||
88b66af84e | |||
233a5eb9d5 | |||
f08f97f267 | |||
18dabe1146 | |||
ba54300e6c | |||
8f539713f6 | |||
c3fd29812f | |||
720cfe0e5b | |||
e3e9af505f | |||
822c03c1b4 | |||
11e1991ccf | |||
998d8025f8 | |||
20d424b8d5 | |||
30133c1198 | |||
d8170d10e5 | |||
123b750e78 | |||
bda976bd1b | |||
588e536077 | |||
94680c95e1 | |||
5c1f8f0cae | |||
e8032b665a | |||
592874680c | |||
8dba6f2fd9 | |||
5dd7dc0fdc | |||
ebd8d15879 | |||
e3b3b09fe2 | |||
dbc6323ab6 | |||
bc89528be6 | |||
173b5bd217 | |||
47baed7d6f | |||
![]() |
7af3cd11f3 | ||
![]() |
ee86ec8cfe | ||
08c8a25966 | |||
b65e6af93a | |||
67a48b74f9 | |||
5508d22608 | |||
701591e5be | |||
e5e3dfe098 | |||
e804712aa0 | |||
bc42d8815b | |||
13a45d9562 | |||
a85442f57d | |||
904ac31a97 | |||
26ebf5a3cb | |||
c77d52e242 | |||
4f78d48b10 | |||
e90b94a694 | |||
c6fe8658d9 | |||
6e5d050de3 | |||
c1f6afe563 | |||
440d6c72d8 | |||
b99311bd7c | |||
41340f0f45 | |||
52720c2748 | |||
7bf88324e2 | |||
17b2511217 | |||
e548d85841 | |||
8c442de4fa | |||
fc339f46ce | |||
4aa1454fc5 | |||
a414c7b875 | |||
f8a0964272 | |||
9878a4f043 | |||
0c2a5cb256 | |||
c2208c54ac | |||
f4964244ec | |||
c84a92f417 | |||
e504123cef | |||
58213cf8c9 | |||
036648b5e4 | |||
54e0b13ba7 | |||
![]() |
1f9091a0e4 | ||
![]() |
6ad49cb22a | ||
![]() |
ffffb1348b | ||
![]() |
7939703bac | ||
2402716a6d | |||
9ef0046e9a | |||
a9d2039136 | |||
1190b13d91 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
/target
|
||||
/result
|
||||
/result
|
||||
/build
|
||||
/config.yml
|
||||
/bRAC
|
1137
Cargo.lock
generated
1137
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@ -1,16 +1,30 @@
|
||||
[package]
|
||||
name = "bRAC"
|
||||
version = "0.1.1+2.0"
|
||||
version = "0.1.4+2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
colored = "3.0.0"
|
||||
lazy_static = "1.5.0"
|
||||
crossterm = "0.28.1"
|
||||
serde = { version = "1.0.217", features = ["serde_derive"] }
|
||||
serde_yml = "0.0.12"
|
||||
homedir = "0.3.4"
|
||||
clap = { version = "4.5.29", features = ["derive"] }
|
||||
native-tls = "0.2.13"
|
||||
native-tls = "0.2.14"
|
||||
clap = { version = "4.5.36", features = ["derive"] }
|
||||
serde = { version = "1.0.219", features = ["serde_derive"] }
|
||||
gtk4 = { version = "0.9.6", features = [ "v4_10" ] }
|
||||
chrono = "0.4.40"
|
||||
serde_default = "0.2.0"
|
||||
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]
|
||||
default = []
|
||||
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
|
||||
winapi = ["dep:winapi"]
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1.20"
|
||||
|
36
Makefile
Normal file
36
Makefile
Normal 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
|
87
README.md
87
README.md
@ -1,21 +1,26 @@
|
||||
# bRAC
|
||||
# 
|
||||
<!--
|
||||
[<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://img.shields.io/badge/Bitcoin-000?style=for-the-badge&logo=bitcoin&logoColor=white">](https://meex.lol/bitcoin)
|
||||
-->
|
||||
|
||||
better RAC client
|
||||
|
||||
## features
|
||||
|
||||
- cheat commands (type /help)
|
||||
- no ip and date visible
|
||||
- uses TOR proxy server by default
|
||||
- plays sound when users receive your messages
|
||||
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
|
||||
- configurable message format
|
||||
- gtk4 GUI
|
||||
- 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)
|
||||
- chunked reading messages
|
||||
|
||||

|
||||

|
||||
|
||||
## how to run
|
||||
|
||||
@ -25,71 +30,51 @@ go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download
|
||||
|
||||
### build from source
|
||||
|
||||
(you have to install [rust](https://www.rust-lang.org/tools/install) at first)
|
||||
1. Make sure [Rust](https://www.rust-lang.org/tools/install) is installed
|
||||
|
||||
2. Clone repository
|
||||
```bash
|
||||
git clone https://github.com/MeexReay/bRAC.git
|
||||
cd bRAC
|
||||
cargo build --release # build release (target/release/bRAC)
|
||||
cargo run # run (builds and runs bRAC itself)
|
||||
```
|
||||
|
||||
## 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: false # enable users' ip viewing
|
||||
disable_ip_hiding: false # disable your ip hiding
|
||||
enable_auth: false # enable auth-mode
|
||||
enable_ssl: false # enable ssl connection
|
||||
enable_chunked: false # enable chunked reading
|
||||
3. Run with Cargo
|
||||
```bash
|
||||
cargo build -r # build release (target/release/bRAC)
|
||||
cargo run -r # build and run
|
||||
```
|
||||
|
||||
## command-line options
|
||||
### nix package
|
||||
|
||||
```
|
||||
-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
|
||||
If you have Nix package manager installed, you can use:
|
||||
|
||||
```bash
|
||||
nix build github:MeexReay/bRAC # build release (result/bin/bRAC)
|
||||
nix run github:MeexReay/bRAC # build and run
|
||||
```
|
||||
|
||||
## cheat commands
|
||||
## chat commands
|
||||
|
||||
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
|
||||
|
||||
- `/help` - show help message \
|
||||
- `/clear` - clear chat \
|
||||
- `/spam *args` - spam with text \
|
||||
- `/help` - show help message
|
||||
- `/register password` - try to register account
|
||||
- `/login password` - login to account
|
||||
- `/clear` - clear chat
|
||||
- `/spam *args` - spam with text
|
||||
- `/ping` - get server ping (send + read)
|
||||
|
||||
## docs
|
||||
|
||||
- [Message formats](https://github.com/MeexReay/bRAC/blob/main/docs/message_formats.md)
|
||||
- [Authenticated mode](https://github.com/MeexReay/bRAC/blob/main/docs/auth_mode.md)
|
||||
- [Message formats](docs/message_formats.md)
|
||||
- [Authenticated mode](docs/auth_mode.md)
|
||||
- [Cross compile](docs/cross_compile.md)
|
||||
- [FAQ](docs/faq.md)
|
||||
|
||||
## see also
|
||||
|
||||
- [RAC-Hub - all about RAC protocol](https://forbirdden.github.io/RAC-Hub/)
|
||||
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
|
||||
- [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
|
||||
- [Mefidroniy - client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client)
|
||||
|
16
build.rs
Normal file
16
build.rs
Normal 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
15
docs/cross_compile.md
Normal 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
7
docs/faq.md
Normal file
@ -0,0 +1,7 @@
|
||||
# FAQ
|
||||
|
||||
## What is RAC protocol
|
||||
|
||||
Sugoma’s “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)
|
63
flake.lock
generated
63
flake.lock
generated
@ -1,5 +1,23 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1743550720,
|
||||
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@ -13,34 +31,48 @@
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
"id": "flake-utils",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739055578,
|
||||
"narHash": "sha256-2MhC2Bgd06uI1A0vkdNUyDYsMD0SLNGKtD8600mZ69A=",
|
||||
"owner": "NixOS",
|
||||
"lastModified": 1744463964,
|
||||
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a45fa362d887f4d4a7157d95c28ca9ce2899b70e",
|
||||
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.11",
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1743296961,
|
||||
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"lastModified": 1744536153,
|
||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -52,6 +84,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
@ -62,11 +95,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1739240901,
|
||||
"narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=",
|
||||
"lastModified": 1744684506,
|
||||
"narHash": "sha256-pDPDMT1rdkTWi8MIoZ67gT3L817R7P0Jo+PP+BrnyJI=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c",
|
||||
"rev": "47beae969336c05e892e1e4a9dbaac9593de34ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
52
flake.nix
52
flake.nix
@ -1,33 +1,43 @@
|
||||
{
|
||||
description = "bRAC - better RAC client";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
|
||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rust-bin.stable.latest.default
|
||||
pkg-config
|
||||
];
|
||||
};
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "bRAC";
|
||||
version = "0.1.2+2.0";
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
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 ];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
30
install.bat
Normal file
30
install.bat
Normal 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
11
install.sh
Executable 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
|
12
ru.themixray.bRAC.desktop
Normal file
12
ru.themixray.bRAC.desktop
Normal 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
BIN
ru.themixray.bRAC.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
544
src/chat.rs
544
src/chat.rs
@ -1,544 +0,0 @@
|
||||
use std::{cmp::{max, min}, error::Error, io::{stdout, Write}, sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock}, thread, time::{Duration, SystemTime}};
|
||||
|
||||
use colored::{Color, Colorize};
|
||||
use crossterm::{cursor::{MoveLeft, MoveRight}, event::{self, Event, KeyCode, KeyModifiers, MouseEventKind}, execute, terminal::{self, disable_raw_mode, enable_raw_mode}};
|
||||
use rand::random;
|
||||
|
||||
use crate::{proto::{connect, send_message_auth}, util::{char_index_to_byte_index, string_chunks}, IP_REGEX};
|
||||
|
||||
use super::{proto::read_messages, util::sanitize_text, COLORED_USERNAMES, DATE_REGEX, config::Context, proto::send_message};
|
||||
|
||||
|
||||
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, messages: Vec<String>, packet_size: usize) {
|
||||
self.packet_size.store(packet_size, Ordering::SeqCst);
|
||||
*self.messages.write().unwrap() = messages;
|
||||
}
|
||||
|
||||
pub fn append(&self, messages: Vec<String>, packet_size: usize) {
|
||||
self.packet_size.store(packet_size, Ordering::SeqCst);
|
||||
self.messages.write().unwrap().append(&mut messages.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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" {
|
||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?,
|
||||
&prepare_message(ctx.clone(),
|
||||
&format!("\r\x1B[1A{}", " ".repeat(64)).repeat(ctx.max_messages)
|
||||
))?;
|
||||
} else if command == "spam" {
|
||||
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" {
|
||||
write!(stdout(), "Help message:\r
|
||||
/help - show help message\r
|
||||
/clear - clear console\r
|
||||
/spam *args - spam console with text\r
|
||||
/ping - check server ping\r
|
||||
\r
|
||||
Press enter to close")?;
|
||||
stdout().flush()?;
|
||||
} else if command == "ping" {
|
||||
let mut before = ctx.messages.packet_size();
|
||||
let start = SystemTime::now();
|
||||
let message = format!("Checking ping... {:X}", random::<u16>());
|
||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
||||
loop {
|
||||
let data = read_messages(
|
||||
&mut connect(&ctx.host, ctx.enable_ssl)?,
|
||||
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)?, &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
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 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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn format_message(ctx: Arc<Context>, 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()
|
||||
.to_string()+" ";
|
||||
|
||||
let prefix = if ctx.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()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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 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_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
||||
} else {
|
||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
||||
}
|
||||
}
|
||||
} 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.clone(), o)).collect()
|
||||
};
|
||||
|
||||
if ctx.enable_chunked {
|
||||
ctx.messages.append(messages.clone(), size);
|
||||
print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?;
|
||||
} else {
|
||||
ctx.messages.update(messages.clone(), size);
|
||||
print_console(ctx.clone(), messages, &ctx.input.read().unwrap())?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{:?}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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();
|
||||
|
||||
thread::spawn({
|
||||
let ctx = ctx.clone();
|
||||
|
||||
move || {
|
||||
loop {
|
||||
recv_tick(ctx.clone()).expect("Error printing console");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
poll_events(ctx).expect("Error while polling events");
|
||||
}
|
143
src/chat/config.rs
Normal file
143
src/chat/config.rs
Normal 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
84
src/chat/ctx.rs
Normal 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
911
src/chat/gui.rs
Normal 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), ¬if);
|
||||
|
||||
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"), ¬if);
|
||||
}
|
||||
}
|
||||
} 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"), ¬if);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"), ¬if);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
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
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
BIN
src/chat/images/logo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
293
src/chat/mod.rs
Normal file
293
src/chat/mod.rs
Normal 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
|
||||
}
|
3
src/chat/styles/dark.css
Normal file
3
src/chat/styles/dark.css
Normal 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); }
|
3
src/chat/styles/light.css
Normal file
3
src/chat/styles/light.css
Normal 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
24
src/chat/styles/style.css
Normal 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; }
|
239
src/config.rs
239
src/config.rs
@ -1,239 +0,0 @@
|
||||
use std::sync::{atomic::AtomicUsize, Arc, RwLock};
|
||||
#[allow(unused_imports)]
|
||||
use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration};
|
||||
use colored::Colorize;
|
||||
use homedir::my_home;
|
||||
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?", false);
|
||||
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?", false);
|
||||
|
||||
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 {
|
||||
#[allow(unused_variables)]
|
||||
let config_path = Path::new("config.yml").to_path_buf();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let config_path = {
|
||||
let home_dir = my_home().ok().flatten().expect("Config find path error");
|
||||
home_dir.join(".config").join("bRAC").join("config.yml")
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let config_path = {
|
||||
let home_dir = my_home().ok().flatten().expect("Config find path error");
|
||||
home_dir.join(".config").join("bRAC").join("config.yml")
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let config_path = {
|
||||
let appdata = env::var("APPDATA").expect("Config find path error");
|
||||
Path::new(&appdata).join("bRAC").join("config.yml")
|
||||
};
|
||||
|
||||
config_path
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod chat;
|
||||
pub mod proto;
|
59
src/main.rs
59
src/main.rs
@ -1,33 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use colored::Color;
|
||||
use config::{configure, get_config_path, load_config, Args, Context};
|
||||
use proto::{connect, read_messages, send_message};
|
||||
use regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use chat::run_main_loop;
|
||||
|
||||
|
||||
lazy_static! {
|
||||
static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
|
||||
static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
mod config;
|
||||
mod chat;
|
||||
mod proto;
|
||||
mod util;
|
||||
|
||||
use bRAC::proto::{connect, read_messages, send_message};
|
||||
use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
|
||||
use clap::Parser;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "winapi")]
|
||||
unsafe { winapi::um::wincon::FreeConsole() };
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let config_path = get_config_path();
|
||||
@ -37,22 +17,16 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if args.configure {
|
||||
configure(config_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let config = load_config(config_path);
|
||||
|
||||
let ctx = Arc::new(Context::new(&config, &args));
|
||||
let mut config = load_config(config_path);
|
||||
|
||||
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(
|
||||
&mut stream,
|
||||
ctx.max_messages,
|
||||
config.max_messages,
|
||||
0,
|
||||
!ctx.enable_ssl,
|
||||
!config.ssl_enabled,
|
||||
false
|
||||
)
|
||||
.ok().flatten()
|
||||
@ -61,12 +35,21 @@ fn main() {
|
||||
}
|
||||
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
|
||||
args.patch_config(&mut config);
|
||||
|
||||
let ctx = Arc::new(Context::new(&config));
|
||||
|
||||
run_main_loop(ctx.clone());
|
||||
}
|
||||
|
141
src/proto.rs
141
src/proto.rs
@ -1,141 +0,0 @@
|
||||
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::TcpStream};
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
|
||||
pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {}
|
||||
impl<T: Read + Write + Unpin + Send + Sync + Debug> RacStream for T {}
|
||||
|
||||
pub fn connect(host: &str, ssl: bool) -> Result<Box<dyn RacStream>, Box<dyn Error>> {
|
||||
Ok(if ssl {
|
||||
let ip: String = host.split_once(":")
|
||||
.map(|o| o.0)
|
||||
.unwrap_or(host).to_string();
|
||||
|
||||
Box::new(TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()?
|
||||
.connect(&ip, connect(host, false)?)?)
|
||||
} else {
|
||||
Box::new(TcpStream::connect(host)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_message(stream: &mut (impl Read + Write), message: &str) -> Result<(), Box<dyn Error>> {
|
||||
stream.write_all(&[0x01])?;
|
||||
stream.write_all(message.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_message_auth(stream: &mut (impl Read + Write), message: &str) -> Result<(), Box<dyn Error>> {
|
||||
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
|
||||
|
||||
stream.write_all(&[0x02])?;
|
||||
stream.write_all(name.as_bytes())?;
|
||||
stream.write_all(b"\n")?;
|
||||
stream.write_all(name.as_bytes())?;
|
||||
stream.write_all(b"\n")?;
|
||||
stream.write_all(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_auth(stream, &message)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_user(stream: &mut (impl Read + Write), name: &str, password: &str) -> Result<(), Box<dyn Error>> {
|
||||
stream.write_all(&[0x03])?;
|
||||
stream.write_all(name.as_bytes())?;
|
||||
stream.write_all(&[b'\n'])?;
|
||||
stream.write_all(password.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 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 data = if start_null {
|
||||
let mut data = skip_null(stream)?;
|
||||
|
||||
loop {
|
||||
let mut buf = vec![0; 1];
|
||||
stream.read_exact(&mut buf)?;
|
||||
let ch = buf[0];
|
||||
if ch == 0 {
|
||||
break
|
||||
}
|
||||
data.push(ch);
|
||||
}
|
||||
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 = {
|
||||
let data = if start_null {
|
||||
let mut data = skip_null(stream)?;
|
||||
while data.len() < to_read {
|
||||
let mut buf = vec![0; to_read - data.len()];
|
||||
stream.read_exact(&mut buf)?;
|
||||
data.append(&mut buf);
|
||||
}
|
||||
data
|
||||
} else {
|
||||
let mut data = vec![0; to_read];
|
||||
stream.read_exact(&mut data)?;
|
||||
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
267
src/proto/mod.rs
Normal 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
169
src/proto/rac.rs
Normal 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
128
src/proto/wrac.rs
Normal 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)))
|
||||
}
|
80
src/util.rs
80
src/util.rs
@ -1,80 +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 mut out = stdout().lock();
|
||||
out.write_all(prompt.to_string().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
35
uninstall.bat
Normal 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
14
uninstall.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user