mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-05-06 21:48:03 +03:00
Compare commits
44 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 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
/result
|
/result
|
||||||
|
/build
|
||||||
/config.yml
|
/config.yml
|
||||||
|
/bRAC
|
597
Cargo.lock
generated
597
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@ -1,27 +1,30 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bRAC"
|
name = "bRAC"
|
||||||
version = "0.1.3+2.0"
|
version = "0.1.4+2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.9.0"
|
rand = "0.9.1"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
colored = "3.0.0"
|
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
serde_yml = "0.0.12"
|
||||||
|
homedir = "0.3.4"
|
||||||
|
native-tls = "0.2.14"
|
||||||
clap = { version = "4.5.36", features = ["derive"] }
|
clap = { version = "4.5.36", features = ["derive"] }
|
||||||
serde = { version = "1.0.219", features = ["serde_derive"] }
|
serde = { version = "1.0.219", features = ["serde_derive"] }
|
||||||
serde_yml = "0.0.12"
|
gtk4 = { version = "0.9.6", features = [ "v4_10" ] }
|
||||||
crossterm = { version = "0.29.0", optional = true }
|
chrono = "0.4.40"
|
||||||
homedir = { version = "0.3.4", optional = true }
|
serde_default = "0.2.0"
|
||||||
native-tls = { version = "0.2.14", optional = true }
|
socks = "0.3.4"
|
||||||
gtk4 = { version = "0.9.6", optional = true, features = [ "v4_10" ] }
|
libnotify = { version = "1.0.3", optional = true }
|
||||||
cfg-if = "1.0.0"
|
gdk-pixbuf = { version = "0.3.0", optional = true }
|
||||||
chrono = { version = "0.4.40", optional = true }
|
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
|
||||||
|
tungstenite = "0.26.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ssl", "homedir", "gtk_gui"]
|
default = []
|
||||||
tui = ["ssl", "homedir", "pretty_tui"]
|
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
|
||||||
ssl = ["dep:native-tls"]
|
winapi = ["dep:winapi"]
|
||||||
pretty_tui = ["dep:crossterm"]
|
|
||||||
gtk_gui = ["dep:gtk4", "dep:chrono"]
|
[build-dependencies]
|
||||||
homedir = ["dep:homedir"]
|
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
|
99
README.md
99
README.md
@ -1,4 +1,4 @@
|
|||||||
# bRAC
|
# 
|
||||||
<!--
|
<!--
|
||||||
[<img src="https://github.com/user-attachments/assets/f2be5caa-6246-4a6a-9bee-2b53086f9afb" height="30">]()
|
[<img src="https://github.com/user-attachments/assets/f2be5caa-6246-4a6a-9bee-2b53086f9afb" height="30">]()
|
||||||
[<img src="https://github.com/user-attachments/assets/4d35191d-1dbc-4391-a761-6ae7f76ba7af" height="30">]()
|
[<img src="https://github.com/user-attachments/assets/4d35191d-1dbc-4391-a761-6ae7f76ba7af" height="30">]()
|
||||||
@ -10,13 +10,13 @@ better RAC client
|
|||||||
## features
|
## features
|
||||||
|
|
||||||
- gtk4 GUI
|
- gtk4 GUI
|
||||||
- cheat commands (type /help)
|
|
||||||
- no ip and date visible
|
|
||||||
- uses TOR proxy server by default (meex.lol:11234)
|
|
||||||
- plays sound when users receive your messages
|
|
||||||
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
|
|
||||||
- configurable message format
|
|
||||||
- RACv1.99.x and RACv2.0 compatible
|
- RACv1.99.x and RACv2.0 compatible
|
||||||
|
- chat commands (type /help)
|
||||||
|
- no ip and date visible for anyone
|
||||||
|
- uses TOR proxy server by default (meex.lol:11234)
|
||||||
|
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
|
||||||
|
- many command-line options (--help)
|
||||||
|
- rich configuration (--config-path to get file path and --configure to edit)
|
||||||
- RACS compatible (--enable-ssl or in --configure enable SSL)
|
- RACS compatible (--enable-ssl or in --configure enable SSL)
|
||||||
- chunked reading messages
|
- chunked reading messages
|
||||||
|
|
||||||
@ -38,24 +38,10 @@ git clone https://github.com/MeexReay/bRAC.git
|
|||||||
cd bRAC
|
cd bRAC
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Build or run with Cargo
|
3. Run with Cargo
|
||||||
```bash
|
```bash
|
||||||
cargo build -r # build release (target/release/bRAC)
|
cargo build -r # build release (target/release/bRAC)
|
||||||
cargo run -r # run (builds and runs bRAC itself)
|
cargo run -r # build and run
|
||||||
```
|
|
||||||
|
|
||||||
TUI version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build -r --no-default-features -F tui
|
|
||||||
cargo run -r --no-default-features -F tui
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimal version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build -r --no-default-features
|
|
||||||
cargo run -r --no-default-features
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### nix package
|
### nix package
|
||||||
@ -63,77 +49,28 @@ cargo run -r --no-default-features
|
|||||||
If you have Nix package manager installed, you can use:
|
If you have Nix package manager installed, you can use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix build github:MeexReay/bRAC # build binary (result/bin/bRAC)
|
nix build github:MeexReay/bRAC # build release (result/bin/bRAC)
|
||||||
nix run github:MeexReay/bRAC # run (builds and runs bRAC)
|
nix run github:MeexReay/bRAC # build and run
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimal version:
|
## chat commands
|
||||||
|
|
||||||
```bash
|
|
||||||
nix build github:MeexReay/bRAC#bRAC-minimal
|
|
||||||
nix run github:MeexReay/bRAC#bRAC-minimal
|
|
||||||
```
|
|
||||||
|
|
||||||
TUI version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix build github:MeexReay/bRAC#bRAC-tui
|
|
||||||
nix run github:MeexReay/bRAC#bRAC-tui
|
|
||||||
```
|
|
||||||
|
|
||||||
## default config
|
|
||||||
|
|
||||||
get config path - `bRAC --config-path` \
|
|
||||||
reconfigure client - `bRAC --configure`
|
|
||||||
|
|
||||||
```yml
|
|
||||||
host: meex.lol:11234 # server host
|
|
||||||
name: null # user name (null - ask every time)
|
|
||||||
message_format: 리㹰<{name}> {text} # message format
|
|
||||||
update_time: 50 # update chat interval
|
|
||||||
max_messages: 200 # chat messages limit
|
|
||||||
enable_ip_viewing: true # enable users' ip viewing
|
|
||||||
disable_ip_hiding: false # disable your ip hiding
|
|
||||||
enable_auth: true # enable auth-mode
|
|
||||||
enable_ssl: false # enable ssl connection
|
|
||||||
enable_chunked: true # enable chunked reading
|
|
||||||
```
|
|
||||||
|
|
||||||
## command-line options
|
|
||||||
|
|
||||||
```
|
|
||||||
-p, --config-path Print config path
|
|
||||||
-H, --host <HOST> Use specified host
|
|
||||||
-n, --name <NAME> Use specified name
|
|
||||||
-F, --message-format <MESSAGE_FORMAT> Use specified message format
|
|
||||||
-r, --read-messages Print unformatted messages from chat and exit
|
|
||||||
-s, --send-message <MESSAGE> Send unformatted message to chat and exit
|
|
||||||
-f, --disable-formatting Disable message formatting and sanitizing
|
|
||||||
-c, --disable-commands Disable slash commands
|
|
||||||
-i, --disable-ip-hiding Disable ip hiding
|
|
||||||
-v, --enable-users-ip-viewing Enable users IP viewing
|
|
||||||
-C, --configure Configure client
|
|
||||||
-a, --enable-auth Enable authentication
|
|
||||||
-S, --enable-ssl Enable SSL
|
|
||||||
-u, --enable-chunked Enable chunked reading
|
|
||||||
-h, --help Print help
|
|
||||||
-V, --version Print version
|
|
||||||
```
|
|
||||||
|
|
||||||
## cheat commands
|
|
||||||
|
|
||||||
commands are any messages that start with a slash `/` \
|
commands are any messages that start with a slash `/` \
|
||||||
messages starting with a slash are sent to chat only if the `--disable-commands` option is specified
|
messages starting with a slash are sent to chat only if the `--disable-commands` option is specified
|
||||||
|
|
||||||
- `/help` - show help message
|
- `/help` - show help message
|
||||||
|
- `/register password` - try to register account
|
||||||
|
- `/login password` - login to account
|
||||||
- `/clear` - clear chat
|
- `/clear` - clear chat
|
||||||
- `/spam *args` - spam with text
|
- `/spam *args` - spam with text
|
||||||
- `/ping` - get server ping (send + read)
|
- `/ping` - get server ping (send + read)
|
||||||
|
|
||||||
## docs
|
## docs
|
||||||
|
|
||||||
- [Message formats](https://github.com/MeexReay/bRAC/blob/main/docs/message_formats.md)
|
- [Message formats](docs/message_formats.md)
|
||||||
- [Authenticated mode](https://github.com/MeexReay/bRAC/blob/main/docs/auth_mode.md)
|
- [Authenticated mode](docs/auth_mode.md)
|
||||||
|
- [Cross compile](docs/cross_compile.md)
|
||||||
|
- [FAQ](docs/faq.md)
|
||||||
|
|
||||||
## see also
|
## see also
|
||||||
|
|
||||||
|
16
build.rs
Normal file
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)
|
33
flake.lock
generated
33
flake.lock
generated
@ -18,6 +18,23 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "flake-utils",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1744463964,
|
"lastModified": 1744463964,
|
||||||
@ -68,6 +85,7 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rust-overlay": "rust-overlay"
|
"rust-overlay": "rust-overlay"
|
||||||
}
|
}
|
||||||
@ -89,6 +107,21 @@
|
|||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
78
flake.nix
78
flake.nix
@ -7,36 +7,15 @@
|
|||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs:
|
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
|
||||||
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
systems = [ "x86_64-linux" ];
|
|
||||||
perSystem = { config, self', pkgs, lib, system, ... }:
|
|
||||||
let
|
let
|
||||||
devDeps = with pkgs; [ pkg-config openssl gtk4 pango ];
|
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
|
||||||
|
|
||||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
msrv = cargoToml.package.rust-version;
|
overlays = [ (import rust-overlay) ];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
rustPackage = { version, features, deps }:
|
inherit system overlays;
|
||||||
(pkgs.makeRustPlatform {
|
|
||||||
cargo = pkgs.rust-bin.stable.latest.minimal;
|
|
||||||
rustc = pkgs.rust-bin.stable.latest.minimal;
|
|
||||||
}).buildRustPackage {
|
|
||||||
inherit (cargoToml.package) name;
|
|
||||||
src = ./.;
|
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
|
||||||
version = lib.concatStrings [ cargoToml.package.version version ];
|
|
||||||
buildNoDefaultFeatures = true;
|
|
||||||
buildFeatures = features;
|
|
||||||
buildInputs = deps;
|
|
||||||
nativeBuildInputs = deps;
|
|
||||||
patchPhase = ''
|
|
||||||
substituteInPlace Cargo.toml --replace \
|
|
||||||
'version = "${cargoToml.package.version}"' \
|
|
||||||
'version = "${lib.concatStrings [ cargoToml.package.version version ]}"'
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mkDevShell = rustc:
|
mkDevShell = rustc:
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
@ -46,38 +25,19 @@
|
|||||||
nativeBuildInputs = devDeps ++ [ rustc ];
|
nativeBuildInputs = devDeps ++ [ rustc ];
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
_module.args.pkgs = import inputs.nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [ (import inputs.rust-overlay) ];
|
|
||||||
};
|
|
||||||
|
|
||||||
packages.default = self'.packages.bRAC;
|
|
||||||
devShells.default = self'.devShells.stable;
|
|
||||||
|
|
||||||
packages.bRAC = (rustPackage {
|
|
||||||
version = "-gtk";
|
|
||||||
features = "ssl homedir gtk_gui";
|
|
||||||
deps = with pkgs; [
|
|
||||||
pkg-config
|
|
||||||
openssl
|
|
||||||
gtk4
|
|
||||||
pango
|
|
||||||
];
|
|
||||||
});
|
|
||||||
packages.bRAC-tui = (rustPackage {
|
|
||||||
version = "";
|
|
||||||
features = "default";
|
|
||||||
deps = with pkgs; [ pkg-config openssl ];
|
|
||||||
});
|
|
||||||
packages.bRAC-minimal = (rustPackage {
|
|
||||||
version = "-minimal";
|
|
||||||
features = "";
|
|
||||||
deps = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
|
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
|
||||||
devShells.stable = (mkDevShell pkgs.rust-bin.stable.latest.default);
|
devShells.default = (mkDevShell pkgs.rust-bin.stable.latest.default);
|
||||||
devShells.msrv = (mkDevShell pkgs.rust-bin.stable.${msrv}.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
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
313
src/chat.rs
313
src/chat.rs
@ -1,313 +0,0 @@
|
|||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock},
|
|
||||||
time::{SystemTime, UNIX_EPOCH}
|
|
||||||
};
|
|
||||||
|
|
||||||
use colored::{Color, Colorize};
|
|
||||||
|
|
||||||
use crate::proto::{register_user, send_message_auth};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
proto::{connect, read_messages, send_message, send_message_spoof_auth},
|
|
||||||
util::sanitize_text,
|
|
||||||
config::Context
|
|
||||||
};
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use regex::Regex;
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
|
|
||||||
pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap();
|
|
||||||
|
|
||||||
pub static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![
|
|
||||||
(Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), Color::Green), // bRAC
|
|
||||||
(Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed), // CRAB
|
|
||||||
(Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), Color::Magenta), // Mefidroniy
|
|
||||||
(Regex::new(r"<(.*?)> (.*)").unwrap(), Color::Cyan), // clRAC
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
cfg_if! {
|
|
||||||
if #[cfg(feature = "pretty_tui")] {
|
|
||||||
mod pretty_tui;
|
|
||||||
pub use pretty_tui::*;
|
|
||||||
} else if #[cfg(feature = "gtk_gui")] {
|
|
||||||
mod gtk_gui;
|
|
||||||
pub use gtk_gui::*;
|
|
||||||
} else {
|
|
||||||
mod minimal_tui;
|
|
||||||
pub use minimal_tui::*;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ChatStorage {
|
|
||||||
messages: RwLock<Vec<String>>,
|
|
||||||
packet_size: AtomicUsize
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChatStorage {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
ChatStorage {
|
|
||||||
messages: RwLock::new(Vec::new()),
|
|
||||||
packet_size: AtomicUsize::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn packet_size(&self) -> usize {
|
|
||||||
self.packet_size.load(Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn messages(&self) -> Vec<String> {
|
|
||||||
self.messages.read().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, max_length: usize, messages: Vec<String>, packet_size: usize) {
|
|
||||||
self.packet_size.store(packet_size, Ordering::SeqCst);
|
|
||||||
let mut messages = messages;
|
|
||||||
if messages.len() > max_length {
|
|
||||||
messages.drain(max_length..);
|
|
||||||
}
|
|
||||||
*self.messages.write().unwrap() = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_and_store(&self, max_length: usize, messages: Vec<String>, packet_size: usize) {
|
|
||||||
self.packet_size.store(packet_size, Ordering::SeqCst);
|
|
||||||
self.append(max_length, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(&self, max_length: usize, messages: Vec<String>) {
|
|
||||||
self.messages.write().unwrap().append(&mut messages.clone());
|
|
||||||
if self.messages.read().unwrap().len() > max_length {
|
|
||||||
self.messages.write().unwrap().drain(max_length..);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const HELP_MESSAGE: &str = "Help message:
|
|
||||||
/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 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(&mut connect(&ctx.host, ctx.enable_ssl)?, "\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(&mut connect(&ctx.host, ctx.enable_ssl)?, &("\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(&mut connect(&ctx.host, ctx.enable_ssl)?, &ctx.name, pass) {
|
|
||||||
Ok(true) => {
|
|
||||||
add_message(ctx.clone(), "you was registered successfully bro")?;
|
|
||||||
*ctx.chat().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.chat().registered.write().unwrap() = Some(pass.to_string());
|
|
||||||
} else if command == "ping" {
|
|
||||||
let mut before = ctx.chat().messages.packet_size();
|
|
||||||
let message = format!("Checking ping... {:X}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis());
|
|
||||||
|
|
||||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
|
||||||
|
|
||||||
let start = SystemTime::now();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(context: Arc<Context>, message: &str) -> String {
|
|
||||||
format!("{}{}{}",
|
|
||||||
if !context.disable_hiding_ip {
|
|
||||||
"\r\x07"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
message,
|
|
||||||
if !context.disable_hiding_ip {
|
|
||||||
let spaces = if context.enable_auth {
|
|
||||||
39
|
|
||||||
} else {
|
|
||||||
54
|
|
||||||
};
|
|
||||||
|
|
||||||
if message.chars().count() < spaces {
|
|
||||||
" ".repeat(spaces-message.chars().count())
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
if message.starts_with("/") && !ctx.disable_commands {
|
|
||||||
on_command(ctx.clone(), &message)?;
|
|
||||||
} else {
|
|
||||||
let message = prepare_message(
|
|
||||||
ctx.clone(),
|
|
||||||
&ctx.message_format
|
|
||||||
.replace("{name}", &ctx.name)
|
|
||||||
.replace("{text}", &message)
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(password) = ctx.chat().registered.read().unwrap().clone() {
|
|
||||||
send_message_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &ctx.name, &password, &message)?;
|
|
||||||
} else if ctx.enable_auth {
|
|
||||||
send_message_spoof_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
|
||||||
} else {
|
|
||||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// message -> (date, ip, text)
|
|
||||||
pub fn parse_message(message: String) -> Option<(String, Option<String>, String, Option<(String, Color)>)> {
|
|
||||||
let message = sanitize_text(&message);
|
|
||||||
|
|
||||||
let message = message
|
|
||||||
.trim_start_matches("(UNREGISTERED)")
|
|
||||||
.trim_start_matches("(UNAUTHORIZED)")
|
|
||||||
.trim_start_matches("(UNAUTHENTICATED)")
|
|
||||||
.trim()
|
|
||||||
.to_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 (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))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_message(enable_ip_viewing: bool, message: String) -> Option<String> {
|
|
||||||
if let Some((date, ip, content, nick)) = parse_message(message.clone()) {
|
|
||||||
Some(format!(
|
|
||||||
"{} {}{}",
|
|
||||||
if enable_ip_viewing {
|
|
||||||
if let Some(ip) = ip {
|
|
||||||
format!("{}{} [{}]", ip, " ".repeat(if 15 >= ip.chars().count() {15-ip.chars().count()} else {0}), date)
|
|
||||||
} else {
|
|
||||||
format!("{} [{}]", " ".repeat(15), date)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("[{}]", date)
|
|
||||||
}.white().dimmed(),
|
|
||||||
nick.map(|(name, color)|
|
|
||||||
format!("<{}> ", name)
|
|
||||||
.color(color)
|
|
||||||
.bold()
|
|
||||||
.to_string()
|
|
||||||
).unwrap_or_default(),
|
|
||||||
content.white().blink()
|
|
||||||
))
|
|
||||||
} else if !message.is_empty() {
|
|
||||||
Some(message.bright_white().to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// message -> (nick, content, color)
|
|
||||||
pub fn find_username_color(message: &str) -> Option<(String, String, Color)> {
|
|
||||||
for (re, color) in COLORED_USERNAMES.iter() {
|
|
||||||
if let Some(captures) = re.captures(message) {
|
|
||||||
return Some((captures[1].to_string(), captures[2].to_string(), color.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_chat(ctx: Arc<Context>, chat: ChatContext) {
|
|
||||||
*ctx.chat.write().unwrap() = Some(Arc::new(chat));
|
|
||||||
}
|
|
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)
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
}
|
@ -1,634 +0,0 @@
|
|||||||
use std::sync::{Arc, RwLock, mpsc::{channel, Sender, Receiver}};
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use chrono::Local;
|
|
||||||
use colored::{Color, Colorize};
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
use gtk4::{
|
|
||||||
self as gtk, gdk::{Cursor, Display, Texture}, gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader}, gio::{
|
|
||||||
self, ActionEntry, ApplicationFlags,
|
|
||||||
MemoryInputStream, Menu
|
|
||||||
}, glib::{
|
|
||||||
self, clone, clone::Downgrade, idle_add_local, idle_add_local_once, source::timeout_add_local_once, timeout_add_local, ControlFlow
|
|
||||||
}, pango::WrapMode, prelude::*, AboutDialog, AlertDialog, Align, Application, ApplicationWindow, Box as GtkBox,
|
|
||||||
Button, Calendar, CssProvider, Entry, Fixed, Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::config::Context;
|
|
||||||
use crate::proto::{connect, read_messages};
|
|
||||||
|
|
||||||
use super::{format_message, on_send_message, parse_message, set_chat, ChatStorage};
|
|
||||||
|
|
||||||
pub struct ChatContext {
|
|
||||||
pub messages: Arc<ChatStorage>,
|
|
||||||
pub registered: Arc<RwLock<Option<String>>>,
|
|
||||||
pub sender: Sender<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiModel {
|
|
||||||
chat_box: GtkBox,
|
|
||||||
chat_scrolled: ScrolledWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local!(
|
|
||||||
static GLOBAL: RefCell<Option<(UiModel, Receiver<String>)>> = RefCell::new(None);
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn add_chat_message(ctx: Arc<Context>, message: String) {
|
|
||||||
let _ = ctx.chat().sender.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![message.clone()]);
|
|
||||||
add_chat_message(ctx.clone(), message);
|
|
||||||
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.chat().messages.packet_size(),
|
|
||||||
!ctx.enable_ssl,
|
|
||||||
ctx.enable_chunked
|
|
||||||
) {
|
|
||||||
Ok(Some((messages, size))) => {
|
|
||||||
let messages: Vec<String> = if ctx.disable_formatting {
|
|
||||||
messages
|
|
||||||
} else {
|
|
||||||
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ctx.enable_chunked {
|
|
||||||
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
||||||
for msg in messages {
|
|
||||||
add_chat_message(ctx.clone(), msg.clone());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
||||||
for msg in messages {
|
|
||||||
add_chat_message(ctx.clone(), msg.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg.clone()]);
|
|
||||||
add_chat_message(ctx.clone(), msg.clone());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(ctx.update_time as u64));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_pixbuf(data: &[u8]) -> Pixbuf {
|
|
||||||
let loader = PixbufLoader::new();
|
|
||||||
loader.write(data).unwrap();
|
|
||||||
loader.close().unwrap();
|
|
||||||
loader.pixbuf().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_menu(_: Arc<Context>, app: &Application) {
|
|
||||||
let menu = Menu::new();
|
|
||||||
|
|
||||||
let file_menu = Menu::new();
|
|
||||||
file_menu.append(Some("New File"), Some("app.file_new"));
|
|
||||||
file_menu.append(Some("Make a bottleflip"), Some("app.make_bottleflip"));
|
|
||||||
file_menu.append(Some("Export brain to jpeg"), Some("unavailable"));
|
|
||||||
file_menu.append(Some("About"), Some("app.about"));
|
|
||||||
|
|
||||||
let edit_menu = Menu::new();
|
|
||||||
edit_menu.append(Some("Edit File"), Some("app.file_edit"));
|
|
||||||
edit_menu.append(Some("Create a new parallel reality"), Some("app.parallel_reality_create"));
|
|
||||||
|
|
||||||
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("file_new")
|
|
||||||
.activate(move |a: &Application, _, _| {
|
|
||||||
AlertDialog::builder()
|
|
||||||
.message("Successful creatin")
|
|
||||||
.detail("your file was created")
|
|
||||||
.buttons(["ok", "cancel", "confirm", "click"])
|
|
||||||
.build()
|
|
||||||
.show(Some(&a.windows()[0]));
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
ActionEntry::builder("make_bottleflip")
|
|
||||||
.activate(move |a: &Application, _, _| {
|
|
||||||
AlertDialog::builder()
|
|
||||||
.message("Sorry")
|
|
||||||
.detail("bottleflip gone wrong :(")
|
|
||||||
.buttons(["yes", "no"])
|
|
||||||
.build()
|
|
||||||
.show(Some(&a.windows()[0]));
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
ActionEntry::builder("parallel_reality_create")
|
|
||||||
.activate(move |a: &Application, _, _| {
|
|
||||||
AlertDialog::builder()
|
|
||||||
.message("Your new parallel reality has been created")
|
|
||||||
.detail(format!("Your parallel reality code: {}", rand::rng().random_range(1..100)))
|
|
||||||
.buttons(["chocolate"])
|
|
||||||
.build()
|
|
||||||
.show(Some(&a.windows()[0]));
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
ActionEntry::builder("file_edit")
|
|
||||||
.activate(move |a: &Application, _, _| {
|
|
||||||
AlertDialog::builder()
|
|
||||||
.message("Successful editioning")
|
|
||||||
.detail("your file was edited")
|
|
||||||
.buttons(["okey"])
|
|
||||||
.build()
|
|
||||||
.show(Some(&a.windows()[0]));
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.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!("../../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();
|
|
||||||
|
|
||||||
server_list.append(&Label::builder().label("meex.lol:42666").halign(Align::Start).selectable(true).build());
|
|
||||||
server_list.append(&Label::builder().label("meex.lol:11234").halign(Align::Start).selectable(true).build());
|
|
||||||
server_list.append(&Label::builder().label("91.192.22.20:42666").halign(Align::Start).selectable(true).build());
|
|
||||||
|
|
||||||
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!("../../konata.png")));
|
|
||||||
konata.set_size_request(174, 127);
|
|
||||||
|
|
||||||
fixed.put(&konata, 325.0, 4.0);
|
|
||||||
|
|
||||||
let logo = Picture::for_pixbuf(&load_pixbuf(include_bytes!("../../logo.gif")));
|
|
||||||
logo.set_size_request(152, 64);
|
|
||||||
|
|
||||||
let logo_anim = PixbufAnimation::from_stream(
|
|
||||||
&MemoryInputStream::from_bytes(
|
|
||||||
&glib::Bytes::from(include_bytes!("../../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);
|
|
||||||
|
|
||||||
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; }
|
|
||||||
idle_add_local_once(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()).bright_red().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; }
|
|
||||||
idle_add_local_once(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()).bright_red().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);
|
|
||||||
|
|
||||||
idle_add_local({
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
ControlFlow::Break
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let window = ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.title(format!("bRAC - Connected to {} as {}", &ctx.host, &ctx.name))
|
|
||||||
.default_width(500)
|
|
||||||
.default_height(500)
|
|
||||||
.resizable(false)
|
|
||||||
.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();
|
|
||||||
idle_add_local(move || {
|
|
||||||
if let Some(o) = scrolled_window_weak.upgrade() {
|
|
||||||
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size());
|
|
||||||
}
|
|
||||||
ControlFlow::Break
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.present();
|
|
||||||
|
|
||||||
UiModel {
|
|
||||||
chat_scrolled,
|
|
||||||
chat_box
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(ctx: Arc<Context>, ui: UiModel) {
|
|
||||||
let (sender, receiver) = channel();
|
|
||||||
|
|
||||||
set_chat(ctx.clone(), ChatContext {
|
|
||||||
messages: Arc::new(ChatStorage::new()),
|
|
||||||
registered: Arc::new(RwLock::new(None)),
|
|
||||||
sender
|
|
||||||
});
|
|
||||||
|
|
||||||
thread::spawn({
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
|
|
||||||
move || {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = recv_tick(ctx.clone()) {
|
|
||||||
let _ = print_message(ctx.clone(), format!("Print messages error: {}", e.to_string()).bright_red().to_string());
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (tx, rx) = channel();
|
|
||||||
|
|
||||||
GLOBAL.with(|global| {
|
|
||||||
*global.borrow_mut() = Some((ui, rx));
|
|
||||||
});
|
|
||||||
|
|
||||||
thread::spawn({
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
move || {
|
|
||||||
while let Ok(message) = receiver.recv() {
|
|
||||||
let _ = tx.send(message.clone());
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
glib::source::timeout_add_once(Duration::ZERO, move || {
|
|
||||||
GLOBAL.with(|global| {
|
|
||||||
if let Some((ui, rx)) = &*global.borrow() {
|
|
||||||
let message: String = rx.recv().unwrap();
|
|
||||||
on_add_message(ctx.clone(), &ui, message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_css() {
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
provider.load_from_data(&format!(
|
|
||||||
"{}\n{}",
|
|
||||||
if let Some(settings) = Settings::default() {
|
|
||||||
if settings.is_gtk_application_prefer_dark_theme() {
|
|
||||||
".message-content { color:rgb(255, 255, 255); }
|
|
||||||
.message-date { color:rgb(146, 146, 146); }
|
|
||||||
.message-ip { color:rgb(73, 73, 73); }"
|
|
||||||
} else {
|
|
||||||
".message-content { color:rgb(0, 0, 0); }
|
|
||||||
.message-date { color:rgb(41, 41, 41); }
|
|
||||||
.message-ip { color:rgb(88, 88, 88); }"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
"
|
|
||||||
.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-black { color: #2E2E2E; }
|
|
||||||
.message-name-bright-black { color: #555555; }
|
|
||||||
.message-name-red { color: #8B0000; }
|
|
||||||
.message-name-bright-red { color: #FF0000; }
|
|
||||||
.message-name-green { color: #006400; }
|
|
||||||
.message-name-bright-green { color: #00FF00; }
|
|
||||||
.message-name-yellow { color: #8B8B00; }
|
|
||||||
.message-name-bright-yellow { color: #FFFF00; }
|
|
||||||
.message-name-blue { color: #00008B; }
|
|
||||||
.message-name-bright-blue { color: #0000FF; }
|
|
||||||
.message-name-bright-magenta { color: #FF00FF; }
|
|
||||||
.message-name-magenta { color: #8B008B; }
|
|
||||||
.message-name-cyan { color: #008B8B; }
|
|
||||||
.message-name-bright-cyan { color: #00FFFF; }
|
|
||||||
.message-name-white { color: #A9A9A9; }
|
|
||||||
.message-name-bright-white { color: #FFFFFF; }
|
|
||||||
"));
|
|
||||||
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&Display::default().expect("Could not connect to a display."),
|
|
||||||
&provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
|
|
||||||
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.enable_ip_viewing {
|
|
||||||
let ip = Label::builder()
|
|
||||||
.label(ip)
|
|
||||||
.margin_end(10)
|
|
||||||
.halign(Align::Start)
|
|
||||||
.css_classes(["message-ip"])
|
|
||||||
.selectable(true)
|
|
||||||
.wrap(true)
|
|
||||||
.wrap_mode(WrapMode::Char)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
hbox.append(&ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let date = Label::builder()
|
|
||||||
.label(format!("[{date}]"))
|
|
||||||
.halign(Align::Start)
|
|
||||||
.css_classes(["message-date"])
|
|
||||||
.selectable(true)
|
|
||||||
.wrap(true)
|
|
||||||
.wrap_mode(WrapMode::Char)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
hbox.append(&date);
|
|
||||||
|
|
||||||
if let Some((name, color)) = nick {
|
|
||||||
let color = match color {
|
|
||||||
Color::Black => "black",
|
|
||||||
Color::Red => "red",
|
|
||||||
Color::Green => "green",
|
|
||||||
Color::Yellow => "yellow",
|
|
||||||
Color::Blue => "blue",
|
|
||||||
Color::Magenta => "magenta",
|
|
||||||
Color::Cyan => "cyan",
|
|
||||||
Color::White => "white",
|
|
||||||
Color::BrightBlack => "bright-black",
|
|
||||||
Color::BrightRed => "bright-red",
|
|
||||||
Color::BrightGreen => "bright-green",
|
|
||||||
Color::BrightYellow => "bright-yellow",
|
|
||||||
Color::BrightBlue => "bright-blue",
|
|
||||||
Color::BrightMagenta => "bright-magenta",
|
|
||||||
Color::BrightCyan => "bright-cyan",
|
|
||||||
Color::BrightWhite => "bright-white",
|
|
||||||
_ => "unknown"
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = Label::builder()
|
|
||||||
.label(format!("<{name}>"))
|
|
||||||
.halign(Align::Start)
|
|
||||||
.css_classes(["message-name", &format!("message-name-{}", color)])
|
|
||||||
.selectable(true)
|
|
||||||
.wrap(true)
|
|
||||||
.wrap_mode(WrapMode::Char)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
hbox.append(&name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = Label::builder()
|
|
||||||
.label(content)
|
|
||||||
.halign(Align::Start)
|
|
||||||
.css_classes(["message-content"])
|
|
||||||
.selectable(true)
|
|
||||||
.wrap(true)
|
|
||||||
.wrap_mode(WrapMode::Char)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
hbox.append(&content);
|
|
||||||
} else {
|
|
||||||
let content = Label::builder()
|
|
||||||
.label(message)
|
|
||||||
.halign(Align::Start)
|
|
||||||
.css_classes(["message-content"])
|
|
||||||
.selectable(true)
|
|
||||||
.wrap(true)
|
|
||||||
.wrap_mode(WrapMode::Char)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
hbox.append(&content);
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
||||||
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(ctx.clone(), ui);
|
|
||||||
load_css();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
application.connect_startup({
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
|
|
||||||
move |app| {
|
|
||||||
build_menu(ctx.clone(), app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
application.run_with_args::<&str>(&[]);
|
|
||||||
}
|
|
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 |
Before Width: | Height: | Size: 24 KiB 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 |
@ -1,97 +0,0 @@
|
|||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::io::stdout;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
super::{
|
|
||||||
config::Context,
|
|
||||||
proto::{connect, read_messages},
|
|
||||||
util::get_input
|
|
||||||
}, format_message, on_send_message, ChatStorage, set_chat
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ChatContext {
|
|
||||||
pub messages: Arc<ChatStorage>,
|
|
||||||
pub registered: Arc<RwLock<Option<String>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_console(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
||||||
let messages = ctx.chat().messages.messages();
|
|
||||||
|
|
||||||
let mut out = stdout().lock();
|
|
||||||
write!(
|
|
||||||
out,
|
|
||||||
"{}\n{}\n{} ",
|
|
||||||
"\n".repeat(ctx.max_messages - messages.len()),
|
|
||||||
messages
|
|
||||||
.into_iter()
|
|
||||||
.map(|o| o.white().blink().to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n"),
|
|
||||||
">".bright_yellow()
|
|
||||||
);
|
|
||||||
out.flush();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![message]);
|
|
||||||
update_console(ctx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
||||||
set_chat(ctx.clone(), ChatContext {
|
|
||||||
messages: Arc::new(ChatStorage::new()),
|
|
||||||
registered: Arc::new(RwLock::new(None)),
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match connect(&ctx.host, ctx.enable_ssl) {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
match read_messages(
|
|
||||||
&mut stream,
|
|
||||||
ctx.max_messages,
|
|
||||||
ctx.chat().messages.packet_size(),
|
|
||||||
!ctx.enable_ssl,
|
|
||||||
ctx.enable_chunked
|
|
||||||
) {
|
|
||||||
Ok(Some((messages, size))) => {
|
|
||||||
let messages: Vec<String> = if ctx.disable_formatting {
|
|
||||||
messages
|
|
||||||
} else {
|
|
||||||
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ctx.enable_chunked {
|
|
||||||
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
||||||
} else {
|
|
||||||
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Connect error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = update_console(ctx.clone());
|
|
||||||
|
|
||||||
if let Some(message) = get_input("") {
|
|
||||||
if let Err(e) = on_send_message(ctx.clone(), &message) {
|
|
||||||
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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
|
||||||
|
}
|
@ -1,417 +0,0 @@
|
|||||||
use crossterm::{
|
|
||||||
cursor::{MoveLeft, MoveRight},
|
|
||||||
event::{self, Event, KeyCode, KeyModifiers, MouseEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{self, disable_raw_mode, enable_raw_mode}
|
|
||||||
};
|
|
||||||
|
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
cmp::{max, min},
|
|
||||||
error::Error, io::{stdout, Write},
|
|
||||||
sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock},
|
|
||||||
thread,
|
|
||||||
time::Duration
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
super::{
|
|
||||||
config::Context,
|
|
||||||
proto::{connect, read_messages},
|
|
||||||
util::{char_index_to_byte_index, string_chunks}
|
|
||||||
}, format_message, on_send_message, set_chat, ChatStorage
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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.chat().scroll.load(Ordering::SeqCst), messages_size);
|
|
||||||
let scroll_f = ((1f64 - scroll as f64 / (messages_size+1) as f64) * (height-2) as f64).round() as usize+1;
|
|
||||||
|
|
||||||
let messages = if height < messages.len() {
|
|
||||||
if scroll < messages.len() - height {
|
|
||||||
messages[
|
|
||||||
messages.len()-height-scroll..
|
|
||||||
messages.len()-scroll
|
|
||||||
].to_vec()
|
|
||||||
} else {
|
|
||||||
if scroll < messages.len() {
|
|
||||||
messages[
|
|
||||||
0..
|
|
||||||
messages.len()-scroll
|
|
||||||
].to_vec()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
messages
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatted_messages = if ctx.disable_formatting {
|
|
||||||
messages
|
|
||||||
.into_iter()
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
} else {
|
|
||||||
messages
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, (s, l))| {
|
|
||||||
format!("{}{}{}",
|
|
||||||
s,
|
|
||||||
" ".repeat(width - 1 - l),
|
|
||||||
if i == scroll_f {
|
|
||||||
"▐".bright_yellow()
|
|
||||||
} else {
|
|
||||||
"▕".yellow()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = format!(
|
|
||||||
"{}\r\n{} {}",
|
|
||||||
formatted_messages.join("\r\n"),
|
|
||||||
">".bright_yellow(),
|
|
||||||
input
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut out = stdout().lock();
|
|
||||||
write!(out, "{}", text)?;
|
|
||||||
out.flush()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn replace_input(cursor: usize, len: usize, text: &str) {
|
|
||||||
let spaces = if text.chars().count() < len {
|
|
||||||
len-text.chars().count()
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
write!(stdout(),
|
|
||||||
"{}{}{}{}",
|
|
||||||
MoveLeft(1).to_string().repeat(cursor),
|
|
||||||
text,
|
|
||||||
" ".repeat(spaces),
|
|
||||||
MoveLeft(1).to_string().repeat(spaces)
|
|
||||||
).unwrap();
|
|
||||||
stdout().lock().flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_input_left(cursor: usize, len: usize, text: &str, left: usize) {
|
|
||||||
let spaces = if text.chars().count() < len {
|
|
||||||
len-text.chars().count()
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
write!(stdout(),
|
|
||||||
"{}{}{}{}",
|
|
||||||
MoveLeft(1).to_string().repeat(cursor),
|
|
||||||
text,
|
|
||||||
" ".repeat(spaces),
|
|
||||||
MoveLeft(1).to_string().repeat(len-left)
|
|
||||||
).unwrap();
|
|
||||||
stdout().lock().flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_events(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut history: Vec<String> = vec![String::new()];
|
|
||||||
let mut history_cursor: usize = 0;
|
|
||||||
let mut cursor: usize = 0;
|
|
||||||
|
|
||||||
let input = ctx.chat().input.clone();
|
|
||||||
let messages = ctx.chat().messages.clone();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if !event::poll(Duration::from_millis(50)).unwrap_or(false) { continue }
|
|
||||||
|
|
||||||
let event = match event::read() {
|
|
||||||
Ok(i) => i,
|
|
||||||
Err(_) => { continue },
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Key(event) => {
|
|
||||||
match event.code {
|
|
||||||
KeyCode::Enter => {
|
|
||||||
let message = input.read().unwrap().clone();
|
|
||||||
|
|
||||||
if !message.is_empty() {
|
|
||||||
replace_input(cursor, message.chars().count(), "");
|
|
||||||
input.write().unwrap().clear();
|
|
||||||
|
|
||||||
cursor = 0;
|
|
||||||
|
|
||||||
history.push(String::new());
|
|
||||||
history_cursor = history.len()-1;
|
|
||||||
|
|
||||||
if let Err(e) = on_send_message(ctx.clone(), &message) {
|
|
||||||
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print_console(
|
|
||||||
ctx.clone(),
|
|
||||||
messages.messages(),
|
|
||||||
""
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
if cursor == 0 || !(0..=history[history_cursor].len()).contains(&(cursor)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let len = input.read().unwrap().chars().count();
|
|
||||||
let i = char_index_to_byte_index(&history[history_cursor], cursor-1);
|
|
||||||
history[history_cursor].remove(i);
|
|
||||||
*input.write().unwrap() = history[history_cursor].clone();
|
|
||||||
replace_input_left(cursor, len, &history[history_cursor], cursor-1);
|
|
||||||
cursor -= 1;
|
|
||||||
}
|
|
||||||
KeyCode::Delete => {
|
|
||||||
if cursor == 0 || !(0..history[history_cursor].len()).contains(&(cursor)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let len = input.read().unwrap().chars().count();
|
|
||||||
let i = char_index_to_byte_index(&history[history_cursor], cursor);
|
|
||||||
history[history_cursor].remove(i);
|
|
||||||
*input.write().unwrap() = history[history_cursor].clone();
|
|
||||||
replace_input_left(cursor, len, &history[history_cursor], cursor);
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
on_close();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
KeyCode::Up | KeyCode::Down => {
|
|
||||||
history_cursor = if event.code == KeyCode::Up {
|
|
||||||
max(history_cursor, 1) - 1
|
|
||||||
} else {
|
|
||||||
min(history_cursor + 1, history.len() - 1)
|
|
||||||
};
|
|
||||||
let len = input.read().unwrap().chars().count();
|
|
||||||
*input.write().unwrap() = history[history_cursor].clone();
|
|
||||||
replace_input(cursor, len, &history[history_cursor]);
|
|
||||||
cursor = history[history_cursor].chars().count();
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => {
|
|
||||||
let height = terminal::size().unwrap().1 as usize;
|
|
||||||
ctx.chat().scroll.store(min(
|
|
||||||
ctx.chat().scroll.load(Ordering::SeqCst)+height,
|
|
||||||
ctx.chat().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.chat().scroll.store(max(
|
|
||||||
ctx.chat().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.chat().scroll.store(min(
|
|
||||||
ctx.chat().scroll.load(Ordering::SeqCst)+3,
|
|
||||||
ctx.chat().messages.messages().len()
|
|
||||||
), Ordering::SeqCst);
|
|
||||||
print_console(
|
|
||||||
ctx.clone(),
|
|
||||||
messages.messages(),
|
|
||||||
&input.read().unwrap()
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
MouseEventKind::ScrollDown => {
|
|
||||||
ctx.chat().scroll.store(max(ctx.chat().scroll.load(Ordering::SeqCst), 3)-3, Ordering::SeqCst);
|
|
||||||
print_console(
|
|
||||||
ctx.clone(),
|
|
||||||
messages.messages(),
|
|
||||||
&input.read().unwrap()
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
||||||
match read_messages(
|
|
||||||
&mut connect(&ctx.host, ctx.enable_ssl)?,
|
|
||||||
ctx.max_messages,
|
|
||||||
ctx.chat().messages.packet_size(),
|
|
||||||
!ctx.enable_ssl,
|
|
||||||
ctx.enable_chunked
|
|
||||||
) {
|
|
||||||
Ok(Some((messages, size))) => {
|
|
||||||
let messages: Vec<String> = if ctx.disable_formatting {
|
|
||||||
messages
|
|
||||||
} else {
|
|
||||||
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ctx.enable_chunked {
|
|
||||||
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())?;
|
|
||||||
} else {
|
|
||||||
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
||||||
print_console(ctx.clone(), messages, &ctx.chat().input.read().unwrap())?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(ctx.update_time as u64));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_close() {
|
|
||||||
disable_raw_mode().unwrap();
|
|
||||||
execute!(stdout(), event::DisableMouseCapture).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ChatContext {
|
|
||||||
pub messages: Arc<ChatStorage>,
|
|
||||||
pub input: Arc<RwLock<String>>,
|
|
||||||
pub registered: Arc<RwLock<Option<String>>>,
|
|
||||||
pub scroll: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![message]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
||||||
set_chat(ctx.clone(), ChatContext {
|
|
||||||
messages: Arc::new(ChatStorage::new()),
|
|
||||||
input: Arc::new(RwLock::new(String::new())),
|
|
||||||
registered: Arc::new(RwLock::new(None)),
|
|
||||||
scroll: Arc::new(AtomicUsize::new(0)),
|
|
||||||
});
|
|
||||||
|
|
||||||
enable_raw_mode().unwrap();
|
|
||||||
execute!(stdout(), event::EnableMouseCapture).unwrap();
|
|
||||||
|
|
||||||
if let Err(e) = print_console(ctx.clone(), Vec::new(), &ctx.chat().input.read().unwrap()) {
|
|
||||||
let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::spawn({
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
|
|
||||||
move || {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = recv_tick(ctx.clone()) {
|
|
||||||
let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap());
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = poll_events(ctx.clone()) {
|
|
||||||
let msg = format!("Poll events error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
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; }
|
252
src/config.rs
252
src/config.rs
@ -1,252 +0,0 @@
|
|||||||
use std::{str::FromStr, sync::{Arc, RwLock}};
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration};
|
|
||||||
use colored::Colorize;
|
|
||||||
use rand::random;
|
|
||||||
use serde_yml;
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
use crate::chat::ChatContext;
|
|
||||||
|
|
||||||
use super::util::get_input;
|
|
||||||
|
|
||||||
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
#[serde(default = "default_host")]
|
|
||||||
pub host: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(default = "default_message_format")]
|
|
||||||
pub message_format: String,
|
|
||||||
#[serde(default = "default_update_time")]
|
|
||||||
pub update_time: usize,
|
|
||||||
#[serde(default = "default_max_messages")]
|
|
||||||
pub max_messages: usize,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_ip_viewing: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub disable_ip_hiding: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_auth: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_chunked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_max_messages() -> usize { 200 }
|
|
||||||
fn default_update_time() -> usize { 50 }
|
|
||||||
fn default_host() -> String { "meex.lol:11234".to_string() }
|
|
||||||
fn default_message_format() -> String { MESSAGE_FORMAT.to_string() }
|
|
||||||
|
|
||||||
fn ask_usize(name: impl ToString, default: usize) -> usize {
|
|
||||||
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
|
|
||||||
.and_then(|o| o.parse().ok()).unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_string(name: impl ToString, default: impl ToString + Clone) -> String {
|
|
||||||
ask_string_option(name, default.clone()).unwrap_or(default.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_string_option(name: impl ToString, default: impl ToString) -> Option<String> {
|
|
||||||
let default = default.to_string();
|
|
||||||
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_bool(name: impl ToString, default: bool) -> bool {
|
|
||||||
get_input(format!("{} (Y/N, default: {}) {} ", name.to_string().bold(), if default { "Y" } else { "N" }, ">".bold()).bright_yellow())
|
|
||||||
.map(|o| o.to_lowercase() != "n")
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(path: PathBuf) -> Config {
|
|
||||||
println!("{}", "To configure the client, please answer a few questions. It won't take long.".yellow());
|
|
||||||
println!("{}", "You can reconfigure client in any moment via `bRAC --configure`".yellow());
|
|
||||||
println!("{}", format!("Config stores in path `{}`", path.to_string_lossy()).yellow());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let host = ask_string("Host", default_host());
|
|
||||||
let name = ask_string_option("Name", "ask every time");
|
|
||||||
let update_time = ask_usize("Update interval", default_update_time());
|
|
||||||
let max_messages = ask_usize("Max messages", default_max_messages());
|
|
||||||
let message_format = ask_string("Message format", default_message_format());
|
|
||||||
let enable_ip_viewing = ask_bool("Enable users IP viewing?", true);
|
|
||||||
let disable_ip_hiding = ask_bool("Enable your IP viewing?", false);
|
|
||||||
let enable_auth = ask_bool("Enable auth-mode?", false);
|
|
||||||
let enable_ssl = ask_bool("Enable SSL?", false);
|
|
||||||
let enable_chunked = ask_bool("Enable chunked reading?", true);
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
host,
|
|
||||||
name,
|
|
||||||
message_format,
|
|
||||||
update_time,
|
|
||||||
max_messages,
|
|
||||||
enable_ip_viewing,
|
|
||||||
disable_ip_hiding,
|
|
||||||
enable_auth,
|
|
||||||
enable_ssl,
|
|
||||||
enable_chunked
|
|
||||||
};
|
|
||||||
|
|
||||||
let config_text = serde_yml::to_string(&config).expect("Config save error");
|
|
||||||
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
|
|
||||||
fs::write(&path, config_text).expect("Config save error");
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "Config saved! You can reconfigure it in any moment via `bRAC --configure`".yellow());
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_config(path: PathBuf) -> Config {
|
|
||||||
if !fs::exists(&path).unwrap_or_default() {
|
|
||||||
let config = configure(path.clone());
|
|
||||||
thread::sleep(Duration::from_secs(4));
|
|
||||||
config
|
|
||||||
} else {
|
|
||||||
let config = &fs::read_to_string(&path).expect("Config load error");
|
|
||||||
serde_yml::from_str(config).expect("Config load error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_config_path() -> PathBuf {
|
|
||||||
let mut config_dir = PathBuf::from_str(".").unwrap();
|
|
||||||
|
|
||||||
#[cfg(all(feature = "homedir", 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) = {
|
|
||||||
env::var("APPDATA")
|
|
||||||
.ok()
|
|
||||||
.and_then(|o| Some(PathBuf::from_str(&o).ok()?.join("bRAC")))
|
|
||||||
} {
|
|
||||||
config_dir = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
config_dir.join("config.yml")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct Args {
|
|
||||||
/// Print config path
|
|
||||||
#[arg(short='p', long)]
|
|
||||||
pub config_path: bool,
|
|
||||||
|
|
||||||
/// Use specified host
|
|
||||||
#[arg(short='H', long)]
|
|
||||||
pub host: Option<String>,
|
|
||||||
|
|
||||||
/// Use specified name
|
|
||||||
#[arg(short='n', long)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// Use specified message format
|
|
||||||
#[arg(short='F', long)]
|
|
||||||
pub message_format: Option<String>,
|
|
||||||
|
|
||||||
/// Print unformatted messages from chat and exit
|
|
||||||
#[arg(short='r', long)]
|
|
||||||
pub read_messages: bool,
|
|
||||||
|
|
||||||
/// Send unformatted message to chat and exit
|
|
||||||
#[arg(short='s', long, value_name="MESSAGE")]
|
|
||||||
pub send_message: Option<String>,
|
|
||||||
|
|
||||||
/// Disable message formatting and sanitizing
|
|
||||||
#[arg(short='f', long)]
|
|
||||||
pub disable_formatting: bool,
|
|
||||||
|
|
||||||
/// Disable slash commands
|
|
||||||
#[arg(short='c', long)]
|
|
||||||
pub disable_commands: bool,
|
|
||||||
|
|
||||||
/// Disable ip hiding
|
|
||||||
#[arg(short='i', long)]
|
|
||||||
pub disable_ip_hiding: bool,
|
|
||||||
|
|
||||||
/// Enable users IP viewing
|
|
||||||
#[arg(short='v', long)]
|
|
||||||
pub enable_users_ip_viewing: bool,
|
|
||||||
|
|
||||||
/// Configure client
|
|
||||||
#[arg(short='C', long)]
|
|
||||||
pub configure: bool,
|
|
||||||
|
|
||||||
/// Enable authentication
|
|
||||||
#[arg(short='a', long)]
|
|
||||||
pub enable_auth: bool,
|
|
||||||
|
|
||||||
/// Enable SSL
|
|
||||||
#[arg(short='S', long)]
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
|
|
||||||
/// Enable chunked reading
|
|
||||||
#[arg(short='u', long)]
|
|
||||||
pub enable_chunked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Context {
|
|
||||||
pub chat: Arc<RwLock<Option<Arc<ChatContext>>>>,
|
|
||||||
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 enable_auth: bool,
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
pub enable_chunked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn new(config: &Config, args: &Args) -> Context {
|
|
||||||
Context {
|
|
||||||
chat: Arc::new(RwLock::new(None)),
|
|
||||||
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,
|
|
||||||
enable_auth: args.enable_auth || config.enable_auth,
|
|
||||||
enable_ssl: args.enable_ssl || config.enable_ssl,
|
|
||||||
enable_chunked: args.enable_chunked || config.enable_chunked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chat(&self) -> Arc<ChatContext> {
|
|
||||||
self.chat.read().unwrap().clone().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod util;
|
|
||||||
pub mod proto;
|
pub mod proto;
|
36
src/main.rs
36
src/main.rs
@ -1,12 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use bRAC::config::{configure, get_config_path, load_config, Args, Context};
|
|
||||||
use bRAC::proto::{connect, read_messages, send_message};
|
use bRAC::proto::{connect, read_messages, send_message};
|
||||||
use bRAC::chat::run_main_loop;
|
use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(feature = "winapi")]
|
||||||
|
unsafe { winapi::um::wincon::FreeConsole() };
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let config_path = get_config_path();
|
let config_path = get_config_path();
|
||||||
@ -16,22 +17,16 @@ fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.configure {
|
let mut config = load_config(config_path);
|
||||||
configure(config_path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = load_config(config_path);
|
|
||||||
|
|
||||||
let ctx = Arc::new(Context::new(&config, &args));
|
|
||||||
|
|
||||||
if args.read_messages {
|
if args.read_messages {
|
||||||
let mut stream = connect(&ctx.host, ctx.enable_ssl).expect("Error reading message");
|
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message");
|
||||||
|
|
||||||
print!("{}", read_messages(
|
print!("{}", read_messages(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
ctx.max_messages,
|
config.max_messages,
|
||||||
0,
|
0,
|
||||||
!ctx.enable_ssl,
|
!config.ssl_enabled,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
.ok().flatten()
|
.ok().flatten()
|
||||||
@ -40,12 +35,21 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = &args.send_message {
|
if let Some(message) = &args.send_message {
|
||||||
send_message(&mut connect(&ctx.host, ctx.enable_ssl).expect("Error sending message"), message).expect("Error sending message");
|
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error sending message");
|
||||||
|
|
||||||
|
send_message(
|
||||||
|
&mut stream,
|
||||||
|
message
|
||||||
|
).expect("Error sending message");
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.send_message.is_some() || args.read_messages {
|
if args.send_message.is_some() || args.read_messages {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.patch_config(&mut config);
|
||||||
|
|
||||||
|
let ctx = Arc::new(Context::new(&config));
|
||||||
|
|
||||||
run_main_loop(ctx.clone());
|
run_main_loop(ctx.clone());
|
||||||
}
|
}
|
||||||
|
183
src/proto.rs
183
src/proto.rs
@ -1,183 +0,0 @@
|
|||||||
#![allow(unused)]
|
|
||||||
|
|
||||||
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::TcpStream};
|
|
||||||
|
|
||||||
pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {}
|
|
||||||
impl<T: Read + Write + Unpin + Send + Sync + Debug> RacStream for T {}
|
|
||||||
|
|
||||||
/// Create RAC connection (also you can just TcpStream::connect)
|
|
||||||
///
|
|
||||||
/// host - host string, example: "example.com:12345", "example.com" (default port is 42666)
|
|
||||||
/// ssl - wrap with ssl client, write false if you dont know what it is
|
|
||||||
pub fn connect(host: &str, ssl: bool) -> Result<Box<dyn RacStream>, Box<dyn Error>> {
|
|
||||||
let host = if host.contains(":") {
|
|
||||||
host.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{host}:42666")
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "ssl")]
|
|
||||||
{
|
|
||||||
use native_tls::TlsConnector;
|
|
||||||
|
|
||||||
if ssl {
|
|
||||||
let ip: String = host.split_once(":")
|
|
||||||
.map(|o| o.0.to_string())
|
|
||||||
.unwrap_or(host.clone());
|
|
||||||
|
|
||||||
return Ok(Box::new(TlsConnector::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.danger_accept_invalid_hostnames(true)
|
|
||||||
.build()?
|
|
||||||
.connect(&ip, connect(&host, false)?)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Box::new(TcpStream::connect(host)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send message
|
|
||||||
///
|
|
||||||
/// stream - any stream that can be written to
|
|
||||||
/// message - message text
|
|
||||||
pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
stream.write_all(format!("\x01{message}").as_bytes())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register user
|
|
||||||
///
|
|
||||||
/// stream - any stream that can be written to
|
|
||||||
/// name - user name
|
|
||||||
/// password - user password
|
|
||||||
///
|
|
||||||
/// returns whether the user was registered
|
|
||||||
pub fn register_user(stream: &mut (impl Write + Read), name: &str, password: &str) -> Result<bool, Box<dyn Error>> {
|
|
||||||
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
|
|
||||||
let mut buf = vec![0];
|
|
||||||
if let Ok(1) = stream.read(&mut buf) {
|
|
||||||
Ok(buf[0] == 0)
|
|
||||||
} else {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send message with auth
|
|
||||||
///
|
|
||||||
/// stream - any stream that can be written to
|
|
||||||
/// message - message text
|
|
||||||
/// name - user name
|
|
||||||
/// password - user password
|
|
||||||
///
|
|
||||||
/// returns 0 if the message was sent successfully
|
|
||||||
/// returns 1 if the user does not exist
|
|
||||||
/// returns 2 if the password is incorrect
|
|
||||||
pub fn send_message_auth(stream: &mut (impl Write + Read), name: &str, password: &str, message: &str) -> Result<u8, Box<dyn Error>> {
|
|
||||||
stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?;
|
|
||||||
|
|
||||||
let mut buf = vec![0];
|
|
||||||
if let Ok(1) = stream.read(&mut buf) {
|
|
||||||
Ok(buf[0])
|
|
||||||
} else {
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send message with fake auth
|
|
||||||
///
|
|
||||||
/// Explaination:
|
|
||||||
///
|
|
||||||
/// let (name, message) = message.split("> ") else { return send_message(stream, message) }
|
|
||||||
/// if send_message_auth(name, name, message) != 0 {
|
|
||||||
/// let name = "\x1f" + "name"
|
|
||||||
/// register_user(stream, name, name)
|
|
||||||
/// send_message_spoof_auth(stream, name + "> " + message)
|
|
||||||
/// }
|
|
||||||
pub fn send_message_spoof_auth(stream: &mut (impl Write + Read), message: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) };
|
|
||||||
|
|
||||||
stream.write_all(format!("\x02{name}\n{name}\n{message}").as_bytes())?;
|
|
||||||
|
|
||||||
let mut buf = vec![0; 1];
|
|
||||||
if let Ok(_) = stream.read_exact(&mut buf) {
|
|
||||||
let name = format!("\x1f{name}");
|
|
||||||
register_user(stream, &name, &name)?;
|
|
||||||
let message = format!("{name}> {message}");
|
|
||||||
send_message_spoof_auth(stream, &message)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Skip null bytes and return first non-null byte
|
|
||||||
pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
||||||
loop {
|
|
||||||
let mut buf = vec![0; 1];
|
|
||||||
stream.read_exact(&mut buf)?;
|
|
||||||
if buf[0] != 0 {
|
|
||||||
break Ok(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
|
|
||||||
while vec.ends_with(&[0]) {
|
|
||||||
vec.remove(vec.len()-1);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read messages
|
|
||||||
///
|
|
||||||
/// max_messages - max messages in list
|
|
||||||
/// last_size - last returned packet size
|
|
||||||
/// start_null - start with skipping null bytes
|
|
||||||
/// chunked - is chunked reading enabled
|
|
||||||
///
|
|
||||||
/// returns (messages, packet size)
|
|
||||||
pub fn read_messages(
|
|
||||||
stream: &mut (impl Read + Write),
|
|
||||||
max_messages: usize,
|
|
||||||
last_size: usize,
|
|
||||||
start_null: bool,
|
|
||||||
chunked: bool
|
|
||||||
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
|
|
||||||
stream.write_all(&[0x00])?;
|
|
||||||
|
|
||||||
let packet_size = {
|
|
||||||
let mut data = vec![0; 10];
|
|
||||||
let len = stream.read(&mut data)?;
|
|
||||||
data.truncate(len);
|
|
||||||
|
|
||||||
String::from_utf8(data)?
|
|
||||||
.trim_matches(char::from(0))
|
|
||||||
.parse()?
|
|
||||||
};
|
|
||||||
|
|
||||||
if last_size == packet_size {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let to_read = if !chunked || last_size == 0 {
|
|
||||||
stream.write_all(&[0x01])?;
|
|
||||||
packet_size
|
|
||||||
} else {
|
|
||||||
stream.write_all(format!("\x02{}", last_size).as_bytes())?;
|
|
||||||
packet_size - last_size
|
|
||||||
};
|
|
||||||
|
|
||||||
let packet_data = {
|
|
||||||
let mut data = vec![0; to_read];
|
|
||||||
stream.read_exact(&mut data)?;
|
|
||||||
|
|
||||||
String::from_utf8_lossy(&data).to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let lines: Vec<&str> = packet_data.split("\n").collect();
|
|
||||||
let lines: Vec<String> = lines.clone().into_iter()
|
|
||||||
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 })
|
|
||||||
.map(|o| o.to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Some((lines, packet_size)))
|
|
||||||
}
|
|
267
src/proto/mod.rs
Normal file
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)))
|
||||||
|
}
|
83
src/util.rs
83
src/util.rs
@ -1,83 +0,0 @@
|
|||||||
use std::{io::{stdin, stdout, BufRead, Write}, ops::Range};
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
|
|
||||||
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_matches(regex: &Regex, text: &str) -> Vec<Range<usize>> {
|
|
||||||
regex.find_iter(text).map(|mat| mat.range()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn char_index_to_byte_index(text: &str, char_index: usize) -> usize {
|
|
||||||
text.char_indices().skip(char_index).next().map(|o| o.0).unwrap_or(text.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn string_chunks(text: &str, width: usize) -> Vec<(String, usize)> {
|
|
||||||
let mut norm: Vec<bool> = vec![true; text.chars().count()];
|
|
||||||
|
|
||||||
for range in get_matches(&ANSI_REGEX, text) {
|
|
||||||
for i in range {
|
|
||||||
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
|
|
||||||
norm[index] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for range in get_matches(&CONTROL_CHARS_REGEX, text) {
|
|
||||||
for i in range {
|
|
||||||
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
|
|
||||||
norm[index] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut now_chunk = String::new();
|
|
||||||
let mut chunks = Vec::new();
|
|
||||||
let mut length = 0;
|
|
||||||
|
|
||||||
for (i, b) in norm.iter().enumerate() {
|
|
||||||
if *b {
|
|
||||||
length += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
now_chunk.push(text.chars().skip(i).next().unwrap());
|
|
||||||
|
|
||||||
if length == width {
|
|
||||||
chunks.push((now_chunk.clone(), length));
|
|
||||||
now_chunk.clear();
|
|
||||||
length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !now_chunk.is_empty() {
|
|
||||||
chunks.push((now_chunk.clone(), length));
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sanitize_text(input: &str) -> String {
|
|
||||||
let without_ansi = ANSI_REGEX.replace_all(input, "");
|
|
||||||
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
|
|
||||||
cleaned_text.into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_input(prompt: impl ToString) -> Option<String> {
|
|
||||||
let prompt = prompt.to_string();
|
|
||||||
if !prompt.is_empty() {
|
|
||||||
let mut out = stdout().lock();
|
|
||||||
out.write_all(prompt.as_bytes()).ok()?;
|
|
||||||
out.flush().ok()?;
|
|
||||||
}
|
|
||||||
let input = stdin().lock().lines().next()
|
|
||||||
.map(|o| o.ok())
|
|
||||||
.flatten()?;
|
|
||||||
|
|
||||||
if input.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(input.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
35
uninstall.bat
Normal file
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