Compare commits

..

139 Commits

Author SHA1 Message Date
fa75ca60c4 rac urls and change nicks colors 2025-04-23 00:39:49 +03:00
091c1bca03 flush websocket 2025-04-22 00:27:17 +03:00
73f7c565e1 wrac compatibility (no tests) 2025-04-21 21:24:41 +03:00
06c27aac63 wym you build project in install.sh? 2025-04-21 20:27:32 +03:00
2a3473853b install and uninstall bat 2025-04-21 20:23:45 +03:00
e55d5a7a55 notifications setting and now all gnotifications hide 2025-04-21 20:01:31 +03:00
cb17464d8f desktop files and winapi usage 2025-04-21 16:47:42 +03:00
015d8fca59 some optimizations 2025-04-21 01:18:20 +03:00
cc01dd1e49 close libnotify notifications on active 2025-04-21 01:14:13 +03:00
cdbe45254d libnotify 2025-04-21 00:49:06 +03:00
88b66af84e move read messages error to println 2025-04-20 23:14:37 +03:00
233a5eb9d5 change name colors 2025-04-20 19:12:28 +03:00
f08f97f267 notifications 2025-04-19 23:54:45 +03:00
18dabe1146 makefile upd 2025-04-19 18:42:06 +03:00
ba54300e6c use std::env for windows 2025-04-19 15:32:09 +03:00
8f539713f6 some chunked reading fixes 2025-04-19 18:24:42 +03:00
c3fd29812f ver 0.1.4+2.0 2025-04-19 18:04:30 +03:00
720cfe0e5b remove redudant 2025-04-19 16:16:47 +03:00
e3e9af505f remove dbg 2025-04-19 16:14:20 +03:00
822c03c1b4 resizable 2025-04-19 16:14:08 +03:00
11e1991ccf settings macros 2025-04-19 16:08:14 +03:00
998d8025f8 socks5 proxy and some sugoma-specified stuff 2025-04-19 15:50:03 +03:00
20d424b8d5 faq 2025-04-19 14:03:36 +03:00
30133c1198 connect to server on click in server list 2025-04-19 13:27:03 +03:00
d8170d10e5 makefile update 2025-04-19 11:34:32 +03:00
123b750e78 reset button 2025-04-19 02:38:47 +03:00
bda976bd1b move protocol out of gui 2025-04-19 02:26:10 +03:00
588e536077 gui settings 2025-04-19 02:22:14 +03:00
94680c95e1 ask name fix and some refactor 2025-04-19 01:24:07 +03:00
5c1f8f0cae Merge branch 'main' into gui 2025-04-19 00:52:17 +03:00
e8032b665a make file tarmosheniye 2025-04-19 00:51:34 +03:00
592874680c config refactor and some fixes like always 2025-04-18 23:08:10 +03:00
8dba6f2fd9 readme update 2025-04-17 23:56:55 +03:00
5dd7dc0fdc some fixes ok 2025-04-17 23:54:44 +03:00
ebd8d15879 copy image.png and logo.gif for readme 2025-04-17 23:40:47 +03:00
e3b3b09fe2 remove images from root 2025-04-17 23:38:29 +03:00
dbc6323ab6 gui init commit 2025-04-17 23:36:09 +03:00
bc89528be6 fix readme logo 2025-04-17 22:59:18 +03:00
173b5bd217 try to merge 2025-04-17 22:58:16 +03:00
47baed7d6f move images to assets/ 2025-04-17 22:56:44 +03:00
MeexReay
7af3cd11f3
brac logo 2025-04-17 20:11:06 +03:00
MeexReay
ee86ec8cfe
Update README.md 2025-04-17 20:05:59 +03:00
08c8a25966 oops 2025-04-17 00:43:50 +03:00
b65e6af93a todo in args 2025-04-17 00:41:55 +03:00
67a48b74f9 ver 0.1.3+2.0 2025-04-16 21:44:45 +03:00
5508d22608 fix some windows gtk stuff 2025-04-16 21:40:09 +03:00
701591e5be win conf fix x3 2025-04-16 20:50:00 +03:00
e5e3dfe098 another windows config fix 2025-04-16 20:49:20 +03:00
e804712aa0 windows config fix 2025-04-16 20:45:47 +03:00
bc42d8815b minimal chat fix 2025-04-16 20:11:47 +03:00
13a45d9562 remove shit 2025-04-16 20:00:09 +03:00
a85442f57d gui as default and more ui 2025-04-16 19:51:37 +03:00
904ac31a97 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-04-16 14:30:20 +03:00
26ebf5a3cb da 2025-04-16 14:30:14 +03:00
c77d52e242 light theme 2025-04-16 03:48:40 +03:00
4f78d48b10 fix flake and scroll 2025-04-16 03:09:53 +03:00
e90b94a694 flake gtk features change 2025-04-16 02:45:51 +03:00
c6fe8658d9 flake update 2025-04-16 02:39:09 +03:00
6e5d050de3 disable gtk as default 2025-04-16 02:24:13 +03:00
c1f6afe563 fix deps 2025-04-16 02:21:59 +03:00
440d6c72d8 gtk gui 2025-04-16 02:21:26 +03:00
b99311bd7c register and login command and some fixes i apologize 2025-04-15 02:15:05 +03:00
41340f0f45 remove empty messages and allow non-format messages 2025-04-14 22:12:44 +03:00
52720c2748 ver 0.1.2+2.0 2025-04-14 21:32:34 +03:00
7bf88324e2 format_message remove context 2025-04-14 20:41:25 +03:00
17b2511217 more library tricks 2025-04-14 20:35:57 +03:00
e548d85841 remove zero skipping 2025-04-14 19:16:30 +03:00
8c442de4fa fix chat reading 2025-04-14 18:15:29 +03:00
fc339f46ce allow unused methods in proto.rs 2025-04-14 15:46:49 +03:00
4aa1454fc5 allow non snake case 2025-04-14 15:45:49 +03:00
a414c7b875 add config yml to gitignore 2025-04-14 15:41:44 +03:00
f8a0964272 fix config path in minimal 2025-04-14 15:40:29 +03:00
9878a4f043 send message spoof auth explaination and some returns from proto funcs ye 2025-04-14 14:53:10 +03:00
0c2a5cb256 fix errors printing and some other fixes 2025-04-14 00:57:55 +03:00
c2208c54ac small fix minimal tui and gitignore update 2025-04-14 00:26:37 +03:00
f4964244ec fix minimal flake deps 2025-04-14 00:12:54 +03:00
c84a92f417 brac minimal on flake fix 2025-04-13 23:52:18 +03:00
e504123cef more nix dev 2025-04-13 23:49:00 +03:00
58213cf8c9 massive rewrite 2025-04-13 22:53:07 +03:00
036648b5e4 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-04-12 14:05:48 +03:00
54e0b13ba7 default port - 42666 2025-04-12 14:05:03 +03:00
MeexReay
1f9091a0e4
Update README.md 2025-04-10 00:16:57 +03:00
MeexReay
6ad49cb22a
proxy server specify 2025-02-22 14:28:34 +03:00
MeexReay
ffffb1348b
remove fking backslashes 2025-02-19 00:52:10 +03:00
MeexReay
7939703bac
update readme coz i can 2025-02-17 16:16:40 +03:00
2402716a6d ddddd 2025-02-14 19:46:40 +03:00
9ef0046e9a ye 2025-02-13 13:47:03 +03:00
a9d2039136 fuck auth mode 2025-02-13 13:46:46 +03:00
1190b13d91 change default config 2025-02-13 01:31:30 +03:00
6bf1cd28b9 фывфыв 2025-02-12 17:05:41 +03:00
67a2ed9fa2 command options and config update 2025-02-12 17:00:09 +03:00
95dcb095fb chunked reading 2025-02-12 16:56:20 +03:00
a2be0e3235 SSL support and secure 2025-02-12 15:50:11 +03:00
978fe2265e docs and configs update 2025-02-12 14:31:57 +03:00
3f163abb7c auth refactor 2025-02-12 10:59:57 +03:00
0063e56438 register to send_message_auth 2025-02-12 03:23:10 +03:00
5dd56bbb28 auth param 2025-02-12 03:20:39 +03:00
9d087d315b auth regex 2025-02-12 03:18:17 +03:00
9d5ca69a1f unregistered fix 2025-02-12 03:04:39 +03:00
1b9d890fc8 fix russian in chat 2025-02-12 02:57:45 +03:00
0111c27389 v2.0 and auth abuse 2025-02-12 02:38:52 +03:00
9c2fe34303 200 max msgs 2025-02-12 02:07:14 +03:00
d65b1fa5b6 fix stil 2025-02-12 02:02:46 +03:00
8c9e6a6d2c stilno 2025-02-12 01:59:31 +03:00
7afca71b59 scroll 2025-02-12 01:49:53 +03:00
04bfc95df1 auth test 2025-02-12 00:56:49 +03:00
e3f51cfacc remove scrollbar from disable_formatting mode 2025-02-12 00:36:08 +03:00
9e35bda45d scrollbar fix 2025-02-12 00:30:54 +03:00
4518896f45 scrollbar 2025-02-12 00:30:05 +03:00
6868f0b3eb fix history and add arrows support 2025-02-11 21:58:55 +03:00
0d2bdcbdc7 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-02-11 21:34:59 +03:00
f266172cdf fix history 2025-02-11 21:34:58 +03:00
MeexReay
a27b08a956
docs 2025-02-11 20:53:30 +03:00
47910442b6 flake nix 2025-02-11 20:52:45 +03:00
8d671c7e86 history and rac 2.0 small compatibility 2025-02-11 20:11:17 +03:00
0e850f79c7 idk what to say 2025-02-11 13:03:46 +03:00
df13dde2c8 ver 0.1.1 2025-02-10 23:36:32 +03:00
22b412823d Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-02-10 22:21:34 +03:00
276248a47f configure param 2025-02-10 22:21:33 +03:00
MeexReay
0ec6f9391d
Update readme.md 2025-02-10 22:07:27 +03:00
ea396bab51 ping command 2025-02-10 18:23:51 +03:00
fac7c10e53 remove nekrasivo 2025-02-10 18:21:20 +03:00
6508062c70 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-02-10 18:19:15 +03:00
a84018fd17 view users ip param 2025-02-10 18:19:14 +03:00
MeexReay
f3e29788f4
Update readme.md 2025-02-10 15:59:19 +03:00
MeexReay
f58aa20682
badges 2025-02-10 15:53:26 +03:00
MeexReay
2df6deb498
Rename README.md to readme.md 2025-02-10 15:14:07 +03:00
10974674c5 see also colored usernames 2025-02-10 15:12:45 +03:00
2ec33f7bee colored usernames .md 2025-02-10 15:11:52 +03:00
12329f2d22 ctrl + c terminates 2025-02-10 15:00:26 +03:00
f06e67db95 fix recv_loop perfomance 2025-02-10 14:52:59 +03:00
ce63efeba6 message storing refactor 2025-02-10 14:48:00 +03:00
0d03824d02 fix hiding ip 2025-02-10 14:23:57 +03:00
4d1cad9520 paramaters 2025-02-10 14:22:34 +03:00
07c62f4ad1 configurable message format 2025-02-10 13:13:11 +03:00
071f9e696d configs fix 2025-02-10 13:09:02 +03:00
46b4b84ed4 configs 2025-02-10 13:04:37 +03:00
f23963c8b8 help command 2025-02-10 11:14:15 +03:00
771562f52b puk puk readme update 2025-02-10 10:47:46 +03:00
42 changed files with 3985 additions and 538 deletions

View File

@ -1,16 +0,0 @@
[target.i686-unknown-linux-gnu]
linker = "i686-unknown-linux-gnu-gcc"
ar = "i686-unknown-linux-gnu-ar"
# [target.x86_64-unknown-linux-gnu]
# linker = "x86_64-unknown-linux-gnu-gcc"
# ar = "x86_64-unknown-linux-gnu-ar"
[target.i686-pc-windows-gnu]
linker = "i686-w64-mingw32-gcc"
ar = "i686-w64-mingw32-ar"
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-ar"

6
.gitignore vendored
View File

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

1521
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

36
Makefile Normal file
View File

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

View File

@ -1,71 +1,82 @@
# bRAC
# ![logo](logo.gif)
<!--
[<img src="https://github.com/user-attachments/assets/f2be5caa-6246-4a6a-9bee-2b53086f9afb" height="30">]()
[<img src="https://github.com/user-attachments/assets/4d35191d-1dbc-4391-a761-6ae7f76ba7af" height="30">]()
[<img src="https://img.shields.io/badge/Bitcoin-000?style=for-the-badge&logo=bitcoin&logoColor=white">](https://meex.lol/bitcoin)
-->
better RAC client
## features
- cheat commands
- no ip and date visible
- uses TOR proxy server by default
- plays sound when users receive your messages
- gtk4 GUI
- RACv1.99.x and RACv2.0 compatible
- chat commands (type /help)
- no ip and date visible for anyone
- uses TOR proxy server by default (meex.lol:11234)
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc)
- RACv1.99.x compatible
- many command-line options (--help)
- rich configuration (--config-path to get file path and --configure to edit)
- RACS compatible (--enable-ssl or in --configure enable SSL)
- chunked reading messages
![image](https://github.com/user-attachments/assets/a2858662-50f1-4554-949c-f55addf48fcc)
![screenshot](image.png)
## how to run
### download binary
go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple. \
(there are no releases now, wait for one or [build from source](https://github.com/MeexReay/bRAC#download-binary))
go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple.
### build from source
(you have to install [rust](https://www.rust-lang.org/tools/install) at first)
1. Make sure [Rust](https://www.rust-lang.org/tools/install) is installed
2. Clone repository
```bash
git clone https://github.com/MeexReay/bRAC.git
cd bRAC
cargo build --release # build release (target/release/bRAC)
cargo run # run (builds and runs bRAC itself)
```
## commands
3. Run with Cargo
```bash
cargo build -r # build release (target/release/bRAC)
cargo run -r # build and run
```
`/clear` - clear chat \
`/spam *args` - spam with text
### nix package
## colored usernames
If you have Nix package manager installed, you can use:
### bRAC
```bash
nix build github:MeexReay/bRAC # build release (result/bin/bRAC)
nix run github:MeexReay/bRAC # build and run
```
regex - `\uB9AC\u3E70<(.*?)> (.*)` \
color - green \
example - `리㹰<nick> text`
## chat commands
### CRAB
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
regex - `\u2550\u2550\u2550<(.*?)> (.*)` \
color - light red \
example - `═══<nick> text`
- `/help` - show help message
- `/register password` - try to register account
- `/login password` - login to account
- `/clear` - clear chat
- `/spam *args` - spam with text
- `/ping` - get server ping (send + read)
### Mefedroniy
## docs
regex - `(.*?): (.*)` \
color - light magenta \
example - `nick: text`
### clRAC
regex - `<(.*?)> (.*)` \
color - cyan \
example - `<nick> text`
- [Message formats](docs/message_formats.md)
- [Authenticated mode](docs/auth_mode.md)
- [Cross compile](docs/cross_compile.md)
- [FAQ](docs/faq.md)
## see also
- [RAC protocol (v1.99.2)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
- [RAC-Hub - all about RAC protocol](https://forbirdden.github.io/RAC-Hub/)
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
- [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
- [AlmatyD - server for RAC](https://gitea.bedohswe.eu.org/bedohswe/almatyd)
- [Mefidroniy - client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client)
- [AlmatyD - server for RACv1.0](https://gitea.bedohswe.eu.org/bedohswe/almatyd)
- [RAC protocol (v1.0)](https://bedohswe.eu.org/text/rac/protocol.md.html)

16
build.rs Normal file
View File

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

View File

@ -1,11 +0,0 @@
#!/usr/bin/env bash
TARGETS=(
x86_64-unknown-linux-gnu
i686-unknown-linux-gnu
x86_64-pc-windows-gnu
i686-pc-windows-gnu
)
for TARGET in "${TARGETS[@]}"; do
cargo build --release --target "$TARGET"
echo "$TARGET" built
done

View File

@ -1,5 +0,0 @@
host: meex.lol:11234 # reverse proxy through tor
name: null # username (null - ask every time)
magic_key: "\uB9AC\u3E70" # default bRAC marker
ad_enabled: false # enable sending ad of bRAC above your message
update_time: 50 # update messages interval

8
docs/auth_mode.md Normal file
View File

@ -0,0 +1,8 @@
# auth mode
## differences from the unauthorized mode
### message format
- there is must to be "> " after name ({name}> {text})
- if there is magic key (like 리㹰) then you must add "<" sign after it (not always)

15
docs/cross_compile.md Normal file
View File

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

7
docs/faq.md Normal file
View File

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

47
docs/message_formats.md Normal file
View File

@ -0,0 +1,47 @@
# message formats
## types
### bRAC
this client
```yml
format: "리㹰<{name}> {text}"
regex: "\uB9AC\u3E70<(.*?)> (.*)"
color: "green"
```
### CRAB
[CRAB - client & server for RAC written in java](https://gitea.bedohswe.eu.org/pixtaded/crab)
```yml
format: "═══<{name}> {text}"
regex: "\u2550\u2550\u2550<(.*?)> (.*)"
color: "light red"
```
### Mefedroniy
[Mefidroniy - client for RAC written in rust](https://github.com/OctoBanon-Main/mefedroniy-client)
```yml
format: "°ʘ<{name}> {text}"
regex: "\u00B0\u0298<(.*?)> (.*)"
color: "light magenta"
```
### clRAC
official client
```yml
format: "<{name}> {text}"
regex: "<(.*?)> (.*)"
color: "cyan"
```
## developer notes
in auth-mode, there is must to be `> ` after name (`{name}> {text}`)

129
flake.lock generated Normal file
View File

@ -0,0 +1,129 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1744463964,
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1744684506,
"narHash": "sha256-pDPDMT1rdkTWi8MIoZ67gT3L817R7P0Jo+PP+BrnyJI=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "47beae969336c05e892e1e4a9dbaac9593de34ab",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"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",
"version": 7
}

43
flake.nix Normal file
View File

@ -0,0 +1,43 @@
{
description = "bRAC - better RAC client";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
mkDevShell = rustc:
pkgs.mkShell {
shellHook = ''
export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc}
'';
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ rustc ];
};
in {
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
devShells.default = (mkDevShell pkgs.rust-bin.stable.latest.default);
packages.default = (pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.stable.latest.minimal;
rustc = pkgs.rust-bin.stable.latest.minimal;
}).buildRustPackage {
inherit (cargoToml.package) name version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
buildInputs = devDeps;
nativeBuildInputs = devDeps ++ [ pkgs.rustc ];
};
}
);
}

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

30
install.bat Normal file
View File

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

11
install.sh Executable file
View File

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

BIN
logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

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

BIN
ru.themixray.bRAC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,39 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
let
# Переопределение для mingw32
mingw32WithDwarf = pkgs.pkgsCross.mingw32.buildPackages.gcc.overrideAttrs (oldAttrs: {
configureFlags = [
"--disable-sjlj-exceptions"
"--enable-dwarf2"
];
});
in
pkgs.mkShell {
buildInputs = with pkgs; [
rustup
gcc_multi
pkg-config
zlib
openssl
# Добавляем кросс-компиляторы
pkgsCross.gnu32.buildPackages.gcc
pkgsCross.gnu32.buildPackages.binutils
pkgsCross.gnu64.buildPackages.gcc
pkgsCross.gnu64.buildPackages.binutils
# Переопределённый MinGW для 32-бит Windows
mingw32WithDwarf
# Необходимые библиотеки для Windows
pkgsCross.mingw32.windows.pthreads
pkgsCross.mingw32.windows.mcfgthreads
# 64-битный MinGW и необходимые библиотеки
pkgsCross.mingwW64.buildPackages.gcc
pkgsCross.mingwW64.windows.pthreads
pkgsCross.mingwW64.windows.mcfgthreads
];
}

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

@ -0,0 +1,143 @@
use std::str::FromStr;
use std::{fs, path::PathBuf};
use serde_yml;
use serde_default::DefaultFromSerde;
use clap::Parser;
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
fn default_true() -> bool { true }
pub fn default_max_messages() -> usize { 200 }
pub fn default_update_time() -> usize { 50 }
pub fn default_host() -> String { "meex.lol:11234".to_string() }
pub fn default_message_format() -> String { MESSAGE_FORMAT.to_string() }
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
pub struct Config {
#[serde(default = "default_host")] pub host: String,
#[serde(default)] pub name: Option<String>,
#[serde(default = "default_message_format")] pub message_format: String,
#[serde(default = "default_update_time")] pub update_time: usize,
#[serde(default = "default_max_messages")] pub max_messages: usize,
#[serde(default = "default_true")] pub hide_my_ip: bool,
#[serde(default)] pub show_other_ip: bool,
#[serde(default)] pub auth_enabled: bool,
#[serde(default)] pub ssl_enabled: bool,
#[serde(default = "default_true")] pub chunked_enabled: bool,
#[serde(default = "default_true")] pub formatting_enabled: bool,
#[serde(default = "default_true")] pub commands_enabled: bool,
#[serde(default)] pub wrac_enabled: bool,
#[serde(default)] pub proxy: Option<String>,
#[serde(default = "default_true")] pub notifications_enabled: bool,
}
pub fn get_config_path() -> PathBuf {
let mut config_dir = PathBuf::from_str(".").unwrap();
#[cfg(not(target_os = "windows"))]
if let Some(dir) = {
let home_dir = {
use homedir::my_home;
my_home().ok().flatten()
};
#[cfg(target_os = "linux")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
#[cfg(target_os = "macos")]
let config_dir = {
let home_dir = home_dir.map(|o| o.join("bRAC"));
home_dir.map(|o| o.join(".config"))
};
config_dir
} {
config_dir = dir;
}
#[cfg(target_os = "windows")]
if let Some(dir) = {
use std::env;
env::var("APPDATA")
.ok()
.and_then(|o| Some(PathBuf::from_str(&o).ok()?.join("bRAC")))
} {
config_dir = dir;
}
config_dir.join("config.yml")
}
pub fn load_config(path: PathBuf) -> Config {
if !fs::exists(&path).unwrap_or_default() {
let config = Config::default();
let config_text = serde_yml::to_string(&config).expect("Config save error");
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
fs::write(&path, config_text).expect("Config save error");
config
} else {
let config = &fs::read_to_string(&path).expect("Config load error");
serde_yml::from_str(config).expect("Config load error")
}
}
pub fn save_config(path: PathBuf, config: &Config) {
let config_text = serde_yml::to_string(config).expect("Config save error");
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
fs::write(&path, config_text).expect("Config save error");
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
/// Print config path
#[arg(short='p', long)]
pub config_path: bool,
/// Print unformatted messages from chat and exit
#[arg(short='r', long)]
pub read_messages: bool,
/// Send unformatted message to chat and exit
#[arg(short='s', long, value_name="MESSAGE")]
pub send_message: Option<String>,
#[arg(short='H', long)] pub host: Option<String>,
#[arg(short='n', long)] pub name: Option<String>,
#[arg(long)] pub message_format: Option<String>,
#[arg(long)] pub update_time: Option<usize>,
#[arg(long)] pub max_messages: Option<usize>,
#[arg(long)] pub hide_my_ip: Option<bool>,
#[arg(long)] pub show_other_ip: Option<bool>,
#[arg(long)] pub auth_enabled:Option <bool>,
#[arg(long)] pub ssl_enabled: Option<bool>,
#[arg(long)] pub chunked_enabled: Option<bool>,
#[arg(long)] pub formatting_enabled: Option<bool>,
#[arg(long)] pub commands_enabled: Option<bool>,
#[arg(long)] pub notifications_enabled: Option<bool>,
#[arg(long)] pub wrac_enabled: Option<bool>,
#[arg(long)] pub proxy: Option<String>,
}
impl Args {
pub fn patch_config(&self, config: &mut Config) {
if let Some(v) = self.host.clone() { config.host = v }
if let Some(v) = self.name.clone() { config.name = Some(v) }
if let Some(v) = self.proxy.clone() { config.proxy = Some(v) }
if let Some(v) = self.message_format.clone() { config.message_format = v }
if let Some(v) = self.update_time { config.update_time = v }
if let Some(v) = self.max_messages { config.max_messages = v }
if let Some(v) = self.hide_my_ip { config.hide_my_ip = v }
if let Some(v) = self.show_other_ip { config.show_other_ip = v }
if let Some(v) = self.auth_enabled { config.auth_enabled = v }
if let Some(v) = self.ssl_enabled { config.ssl_enabled = v }
if let Some(v) = self.chunked_enabled { config.chunked_enabled = v }
if let Some(v) = self.formatting_enabled { config.formatting_enabled = v }
if let Some(v) = self.commands_enabled { config.commands_enabled = v }
if let Some(v) = self.notifications_enabled { config.notifications_enabled = v }
if let Some(v) = self.wrac_enabled { config.wrac_enabled = v }
}
}

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

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

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

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

View File

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

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

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

4
src/lib.rs Normal file
View File

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

View File

@ -1,88 +1,55 @@
use std::{
error::Error,
io::{stdin, stdout, BufRead, Write},
sync::{Arc, RwLock},
};
use std::sync::Arc;
use colored::Color;
use rac::{run_recv_loop, send_message};
use rand::random;
use regex::Regex;
use lazy_static::lazy_static;
use term::run_main_loop;
const DEFAULT_HOST: &str = "meex.lol:11234";
const ADVERTISEMENT: &str = "\r\x1B[1A use bRAC client! https://github.com/MeexReay/bRAC \x1B[1B";
const MAX_MESSAGES: usize = 100;
const MAGIC_KEY: &str = "\u{B9AC}\u{3E70}";
const ADVERTISEMENT_ENABLED: bool = false;
const UPDATE_TIME: u64 = 50;
mod term;
mod rac;
lazy_static! {
static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![
(Regex::new(&format!(r"{}<(.*?)> (.*)", MAGIC_KEY)).unwrap(), Color::Green),
(Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed),
(Regex::new(r"(.*?): (.*)").unwrap(), Color::Magenta),
(Regex::new(r"<(.*?)> (.*)").unwrap(), Color::Cyan),
];
}
fn get_input(prompt: &str, default: &str) -> String {
let input = || -> Option<String> {
let mut out = stdout().lock();
out.write_all(prompt.as_bytes()).ok()?;
out.flush().ok()?;
stdin().lock().lines().next()
.map(|o| o.ok())
.flatten()
}();
if let Some(input) = &input {
if input.is_empty() {
default
} else {
input
}
} else {
default
}.to_string()
}
fn on_command(host: &str, command: &str) -> Result<(), Box<dyn Error>> {
let command = command.trim_start_matches("/");
let (command, args) = command.split_once(" ").unwrap_or((&command, ""));
let args = args.split(" ").collect::<Vec<&str>>();
if command == "clear" {
send_message(host, &format!("\r\x1B[1A{}", " ".repeat(64)).repeat(MAX_MESSAGES))?;
// *input.write().unwrap() = "/ заспамлено)))".to_string();
} else if command == "spam" {
send_message(host, &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(MAX_MESSAGES))?;
// *input.write().unwrap() = "/ заспамлено)))".to_string();
}
Ok(())
}
use bRAC::proto::{connect, read_messages, send_message};
use bRAC::chat::{config::{get_config_path, load_config, Args}, ctx::Context, run_main_loop};
use clap::Parser;
fn main() {
let host = get_input(&format!("Host (default: {}) > ", DEFAULT_HOST), DEFAULT_HOST);
let anon_name = format!("Anon#{:X}", random::<u16>());
let name = get_input(&format!("Name (default: {}) > ", anon_name), &anon_name);
#[cfg(feature = "winapi")]
unsafe { winapi::um::wincon::FreeConsole() };
let messages = Arc::new(RwLock::new(String::new()));
let input = Arc::new(RwLock::new(String::new()));
let args = Args::parse();
let config_path = get_config_path();
run_recv_loop(host.clone(), messages.clone(), input.clone());
run_main_loop(messages.clone(), input.clone(), host.clone(), name.clone());
if args.config_path {
print!("{}", config_path.to_string_lossy());
return;
}
let mut config = load_config(config_path);
if args.read_messages {
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message");
print!("{}", read_messages(
&mut stream,
config.max_messages,
0,
!config.ssl_enabled,
false
)
.ok().flatten()
.expect("Error reading messages").0.join("\n")
);
}
if let Some(message) = &args.send_message {
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error sending message");
send_message(
&mut stream,
message
).expect("Error sending message");
}
if args.send_message.is_some() || args.read_messages {
return;
}
args.patch_config(&mut config);
let ctx = Arc::new(Context::new(&config));
run_main_loop(ctx.clone());
}

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

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

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

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

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

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

View File

@ -1,90 +0,0 @@
use std::{error::Error, io::{Read, Write}, net::TcpStream, sync::{Arc, RwLock}, thread, time::Duration};
use super::{ADVERTISEMENT, ADVERTISEMENT_ENABLED, term::print_console, UPDATE_TIME};
pub fn send_message(host: &str, message: &str) -> Result<(), Box<dyn Error>> {
let mut stream = TcpStream::connect(host)?;
stream.write_all(&[0x01])?;
let data = format!("\r\x07{}{}{}",
message,
if message.chars().count() < 39 {
" ".repeat(39-message.chars().count())
} else {
String::new()
},
if ADVERTISEMENT_ENABLED {ADVERTISEMENT} else {""}
);
stream.write_all(data.as_bytes())?;
Ok(())
}
fn skip_null(stream: &mut TcpStream) -> 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)
}
}
}
fn read_messages(host: &str) -> Result<String, Box<dyn Error>> {
let mut stream = TcpStream::connect(host)?;
stream.write_all(&[0x00])?;
let packet_size = {
let mut data = skip_null(&mut stream)?;
loop {
let mut buf = vec![0; 1];
stream.read_exact(&mut buf)?;
let ch = buf[0];
if ch == 0 {
break
}
data.push(ch);
}
String::from_utf8(data)?
.trim_matches(char::from(0))
.parse()?
};
stream.write_all(&[0x01])?;
let packet_data = {
let mut data = skip_null(&mut stream)?;
while data.len() < packet_size {
let mut buf = vec![0; packet_size - data.len()];
let read_bytes = stream.read(&mut buf)?;
buf.truncate(read_bytes);
data.append(&mut buf);
}
String::from_utf8_lossy(&data).to_string()
};
Ok(packet_data)
}
fn recv_loop(host: &str, cache: Arc<RwLock<String>>, input: Arc<RwLock<String>>) -> Result<(), Box<dyn Error>> {
while let Ok(data) = read_messages(host) {
if data == cache.read().unwrap().clone() {
continue
}
*cache.write().unwrap() = data;
print_console(&cache.read().unwrap(), &input.read().unwrap())?;
thread::sleep(Duration::from_millis(UPDATE_TIME));
}
Ok(())
}
pub fn run_recv_loop(host: String, messages: Arc<RwLock<String>>, input: Arc<RwLock<String>>) {
thread::spawn({
move || {
let _ = recv_loop(&host, messages, input);
println!("Connection closed");
}
});
}

View File

@ -1,158 +0,0 @@
use std::{error::Error, io::{stdout, Write}, sync::{Arc, RwLock}, thread, time::Duration};
use colored::{Color, Colorize};
use crossterm::{cursor::MoveLeft, event::{self, Event, KeyCode}, terminal::{disable_raw_mode, enable_raw_mode}, ExecutableCommand};
use regex::Regex;
use crate::{on_command, rac::send_message, ADVERTISEMENT, COLORED_USERNAMES, DATE_REGEX, MAGIC_KEY, MAX_MESSAGES, UPDATE_TIME};
pub fn print_console(messages: &str, input: &str) -> Result<(), Box<dyn Error>> {
let mut messages = messages.split("\n")
.map(|o| o.to_string())
.collect::<Vec<String>>();
messages.reverse();
messages.truncate(MAX_MESSAGES);
messages.reverse();
let messages: Vec<String> = messages.into_iter().filter_map(format_message).collect();
let text = format!(
"{}{}\n> {}",
"\n".repeat(MAX_MESSAGES - messages.len()),
messages.join("\n"),
// if sound { "\x07" } else { "" },
input
);
for line in text.lines() {
write!(stdout().lock(), "\r\n{}", line)?;
stdout().lock().flush()?;
}
Ok(())
}
fn format_message(message: String) -> Option<String> {
let message = message.trim_end_matches(ADVERTISEMENT);
let message = Regex::new(r"\{[^}]*\}\ ").unwrap().replace(&message, "").to_string();
let message = sanitize_text(&message);
if ADVERTISEMENT.len() > 0 &&
message.starts_with(ADVERTISEMENT
.trim_start_matches("\r")
.trim_start_matches("\n")) {
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());
Some(if let Some(captures) = find_username_color(&message) {
let nick = captures.0;
let content = captures.1;
let color = captures.2;
format!(
"{} {} {}",
format!("[{}]", date).white().dimmed(),
format!("<{}>", nick).color(color).bold(),
content.white().blink()
)
} else {
format!(
"{} {}",
format!("[{}]", date).white().dimmed(),
message.white().blink()
)
})
}
fn sanitize_text(input: &str) -> String {
let ansi_regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap();
let control_chars_regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
let without_ansi = ansi_regex.replace_all(input, "");
let cleaned_text = control_chars_regex.replace_all(&without_ansi, "");
cleaned_text.into_owned()
}
/// nick content nick_color
fn find_username_color(message: &str) -> Option<(String, String, Color)> {
for (re, color) in COLORED_USERNAMES.iter() {
if let Some(captures) = re.captures(message) {
return Some((captures[1].to_string(), captures[2].to_string(), color.clone()))
}
}
None
}
fn poll_events(input: Arc<RwLock<String>>, host: String, name: String) {
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();
let input_len = input.read().unwrap().chars().count();
stdout().lock().execute(MoveLeft(input_len as u16)).unwrap();
write!(stdout(), "{}{}", " ".repeat(input_len), MoveLeft(input_len as u16).to_string()).unwrap();
stdout().lock().flush().unwrap();
input.write().unwrap().clear();
if !message.is_empty() {
if message.starts_with("/") {
on_command(&host, &message).expect("Error on command");
} else {
send_message(&host, &format!("{}<{}> {}", MAGIC_KEY, name, message)).expect("Error sending message");
}
}
}
KeyCode::Backspace => {
if input.write().unwrap().pop().is_some() {
stdout().lock().execute(MoveLeft(1)).unwrap();
write!(stdout(), " {}", MoveLeft(1).to_string()).unwrap();
stdout().lock().flush().unwrap();
}
}
KeyCode::Char(c) => {
input.write().unwrap().push(c);
write!(stdout(), "{}", c).unwrap();
stdout().lock().flush().unwrap();
}
KeyCode::Esc => {
disable_raw_mode().unwrap();
break;
},
_ => {}
}
},
Event::Paste(data) => {
input.write().unwrap().push_str(&data);
write!(stdout(), "{}", &data).unwrap();
stdout().lock().flush().unwrap();
}
_ => {}
}
}
}
pub fn run_main_loop(messages: Arc<RwLock<String>>, input: Arc<RwLock<String>>, host: String, name: String) {
enable_raw_mode().unwrap();
thread::spawn({
let messages = messages.clone();
let input = input.clone();
move || {
print_console(
&messages.read().unwrap(),
&input.read().unwrap()
).expect("Error printing console");
thread::sleep(Duration::from_millis(UPDATE_TIME));
}
});
poll_events(input.clone(), host, name);
}

35
uninstall.bat Normal file
View File

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

14
uninstall.sh Executable file
View File

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