Compare commits

...

70 Commits

Author SHA1 Message Date
032f8eb900 fix(docs): fix crate.md example 2025-06-24 16:14:52 +03:00
c18e141880 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-06-24 16:03:46 +03:00
4cb0064678 refactor(config): remove fake auth and fix reset button for new settings 2025-06-24 16:03:36 +03:00
5c4137fe3a docs(readme): add racinfo 2025-06-24 13:06:21 +03:00
887d57e668 refactor(build): i forgot 2025-06-24 13:00:26 +03:00
f4aa86068f fix(build): nixos cross-building 2025-06-23 19:21:53 +03:00
8c7ad0dedb fix(build): fix cross-rs compiling and build.sh 2025-06-22 22:58:39 +03:00
29cb5e246d refactor: remove gvsbuild.zip after extracting 2025-06-22 21:06:34 +03:00
d408ec7272 build: add cross-compiling stuff 2025-06-22 21:04:57 +03:00
3453ca326c Fix: fix the same dumb shit but for Enter ket 2025-06-22 19:36:51 +03:00
a9fb66a8fd Fix: send message freezes 2025-06-22 18:33:12 +03:00
388bfb7672 What 2025-06-22 14:33:10 +03:00
MeexReay
76e30c9a08
Merge pull request #6 from Forbirdden/patch-2
Update user_agents.md
2025-06-22 11:56:33 +03:00
Forbirdden
a9b32a2001
Update user_agents.md 2025-06-22 13:55:01 +05:00
ae3770dcdf Feat: add snowdrop user agent 2025-06-22 09:41:52 +03:00
4f190af7fc update RAC-Hub link 2025-06-21 12:43:53 +03:00
46e74c9bf7 Fix: fix remained images 2025-06-21 07:47:06 +03:00
0042c5a3e1 Fix: images restored after git-lfs and .gitattributes were removed 2025-06-21 07:43:27 +03:00
92a896e46f Style: remove git-lfs 2025-06-21 07:40:10 +03:00
f3b6cbd01c Fix: make websocket message size bigger 2025-06-20 01:58:01 +03:00
956612b192 Fix: make connect timeout greater 2025-06-20 01:28:20 +03:00
16d7cf0b29 Style: align user-agents comments 2025-06-20 01:26:44 +03:00
9a191eec78 Feat: make tor proxy as default 2025-06-20 01:19:47 +03:00
bbbcb93a7f Clean: remove unused import 2025-06-20 01:12:07 +03:00
5b23a3bd70 Fix: fix and improve get_config_path function 2025-06-19 14:59:14 +03:00
c8b2c7e541 write about using as crate 2025-06-19 13:46:06 +03:00
MeexReay
80e7b8c506
Merge pull request #5 from pansangg/main
add cRACk user agent support
2025-06-19 08:36:44 +03:00
pansangg
aaee249f56
Update user_agents.md 2025-06-19 02:50:14 +07:00
pansangg
9675dfe87f
Update mod.rs 2025-06-19 02:48:50 +07:00
pansangg
32cf3839bf
Update user_agents.md 2025-06-19 02:48:29 +07:00
pansangg
5029b51cf6
Update user_agents.md 2025-06-19 02:41:33 +07:00
pansangg
c9673807d9
Update mod.rs 2025-06-19 02:39:10 +07:00
MeexReay
c3982a211c
remove github actions 2025-06-18 21:48:16 +03:00
MeexReay
3a789aae20
add github actions 2025-06-18 21:43:35 +03:00
MeexReay
b19b94b4d9
fix crack description 2025-06-18 21:24:25 +03:00
MeexReay
b58aa445ae
add cRACk 2025-06-18 19:47:14 +03:00
955da8a7b5 add gtk to defaults 2025-06-18 16:45:44 +03:00
7ba01992a8 make gtk feature 2025-06-18 16:44:32 +03:00
29764b0344 respect notifications_enabled setting 2025-06-18 13:11:18 +03:00
a102d0d260 write about rac url 2025-06-18 07:40:57 +03:00
MeexReay
4ee854d8af
fix features 2025-06-18 05:48:14 +03:00
MeexReay
5cbc2351ed
NO USING TOR PROXY BY DEFAULT AHHHH 2025-06-18 05:43:47 +03:00
d2ad18ba89 notify-rust 2025-06-18 03:06:28 +03:00
78e0caa641 fix compiling docs x2 2025-06-18 02:15:13 +03:00
0123589ec7 fix compiling docs 2025-06-18 02:09:51 +03:00
05f0ec22a8 fix gvsbuild link 2025-06-18 01:41:52 +03:00
MeexReay
0b625c33b6
add comma between features in the compiling docs 2025-06-18 01:15:38 +03:00
7406f2cd96 Merge branch 'main' of https://github.com/MeexReay/bRAC 2025-06-18 01:14:14 +03:00
e582677bce set wracs server as default 2025-06-18 01:13:49 +03:00
23d8ebffb5 user agents update 2025-06-17 12:33:34 +03:00
f5c33ca31c add wracs server 2025-06-17 10:38:44 +03:00
d692b8dc99 compiling docs 2025-06-17 08:14:25 +03:00
5d9fdd1719 remove crosscompile, add wrac 2025-06-17 05:17:09 +03:00
2accb6e73d fix -r and -s with other args 2025-06-17 01:46:39 +03:00
c1e9d00d3a rustfmt 2025-06-17 00:03:25 +03:00
3e75662969 remove dbgs 2025-06-16 21:15:08 +03:00
492668bfd5 minor fixes 2025-06-16 21:14:00 +03:00
f615811f90 write about wrac 2025-06-16 09:23:07 +03:00
374dab3951 remove duplicate settings + remove remove_null arg 2025-06-16 09:14:13 +03:00
0260206500 fix bug that freezes window when server hands 2025-06-16 07:55:08 +03:00
9c29d4e742 remake settings gui 2025-06-16 07:09:33 +03:00
efc202153f add gui settings 2025-06-16 07:02:10 +03:00
97948aa420 make out-of-focus update time setting 2025-06-16 06:27:58 +03:00
01b3643c14 add messages as markup label instead of many labels 2025-06-16 06:05:24 +03:00
9ef7560963 add debug logs setting 2025-06-16 04:18:11 +03:00
b206f18829 fix desktop files 2025-06-16 03:44:13 +03:00
2c35c0a18f fix images 2025-06-16 03:25:27 +03:00
89147235dc lfs 2025-06-16 03:17:03 +03:00
d390c1f28b fix makefile install 2025-06-16 02:46:08 +03:00
8c5dad7aa5 add new rules to makefile 2025-06-16 02:22:35 +03:00
43 changed files with 2543 additions and 935 deletions

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}

779
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,19 +12,22 @@ homedir = "0.3.4"
native-tls = "0.2.14" 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"] }
gtk4 = { version = "0.9.6", features = [ "v4_10" ] } gtk4 = { version = "0.9.6", optional = true }
chrono = "0.4.40" chrono = "0.4.40"
serde_default = "0.2.0" serde_default = "0.2.0"
socks = "0.3.4" socks = "0.3.4"
libnotify = { version = "1.0.3", optional = true } libnotify = { version = "1.0.3", optional = true }
notify-rust = { version = "4.11.7", optional = true }
gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] } winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
tungstenite = "0.27.0" tungstenite = "0.27.0"
[features]
default = []
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
winapi = ["dep:winapi"]
[build-dependencies] [build-dependencies]
winresource = "0.1.20" winresource = { version = "0.1.20", optional = true }
[features]
default = ["gtk"]
gtk = ["dep:gtk4"]
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
notify-rust = ["dep:notify-rust"]
winapi = ["dep:winapi", "dep:winresource"]

2
Cross.toml Normal file
View File

@ -0,0 +1,2 @@
[target.x86_64-pc-windows-gnu]
image = "mglolenstine/gtk4-cross:rust-gtk-latest"

View File

@ -1,15 +1,42 @@
.PHONY: clean install uninstall .PHONY: clean install uninstall build_linux build_windows build_all
TARGETS = \
i686-unknown-linux-gnu \
i686-unknown-linux-musl \
x86_64-unknown-linux-none \
x86_64-unknown-linux-gnu \
x86_64-unknown-linux-musl \
aarch64-unknown-linux-gnu \
aarch64-unknown-linux-musl
install: target/release/bRAC install: target/release/bRAC
mkdir -p ~/.local
mkdir -p ~/.local/bin
mkdir -p ~/.local/share
cp $< ~/.local/bin/bRAC cp $< ~/.local/bin/bRAC
chmod +x ~/.local/bin/bRAC chmod +x ~/.local/bin/bRAC
mkdir ~/.local/share/bRAC -p mkdir ~/.local/share/bRAC -p
cp misc/bRAC.png ~/.local/share/bRAC/icon.png cp misc/bRAC.png ~/.local/share/bRAC/icon.png
cp misc/bRAC.desktop ~/.local/share/applications/ru.themixray.bRAC.desktop ./misc/create-desktop.sh > ~/.local/share/applications/ru.themixray.bRAC.desktop
uninstall: uninstall:
rm -rf ~/.config/bRAC ~/.local/share/bRAC rm -rf ~/.config/bRAC ~/.local/share/bRAC
rm -f ~/.local/share/applications/ru.themixray.bRAC.desktop rm -f ~/.local/share/applications/ru.themixray.bRAC.desktop
target/release/bRAC: target/release/bRAC:
cargo build -r cargo build -r
build_all: build_linux build_windows
build_linux:
mkdir -p build
mkdir -p build/linux
for target in $(TARGETS); do \
cargo build -r --target $$target; \
cp target/$$target/bRAC build/linux/$$target-bRAC; \
done
build_windows:
echo "Windows build is in development!!!"
clean: clean:
cargo clean cargo clean
rm -rf build

View File

@ -11,15 +11,15 @@ better RAC client
- gtk4 modern GUI - gtk4 modern GUI
- RACv1.99.x and RACv2.0 compatible - RACv1.99.x and RACv2.0 compatible
- WRAC compatible (can be enabled in the settings) - WRAC compatible ([docs](docs/wrac.md))
- chat commands (type /help) - chat commands (type /help)
- no ip and date visible for anyone - uses tor proxy as default (wracs://meex.lol:11234)
- uses TOR proxy server by default (meex.lol:11234) - no ip and date visible for anyone (almost)
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc) - coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc.)
- many command-line options (--help) - many command-line options (see --help)
- rich configuration (--config-path to get file path and --configure to edit) - rich configuration (--config-path to get file path)
- RACS compatible (--enable-ssl or in --configure enable SSL) - RACS/WRACS compatible (ex: wracs://meex.lol)
- chunked reading messages - reading messages chunked (less traffic usage)
![screenshot](misc/image.png) ![screenshot](misc/image.png)
@ -39,6 +39,8 @@ better RAC client
NO SOLUTION NO SOLUTION
Read [compiling docs](docs/compiling.md) to build it manually.
### download binary ### download binary
go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple. go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple.
@ -59,6 +61,8 @@ cargo build -r # build release (target/release/bRAC)
cargo run -r # build and run cargo run -r # build and run
``` ```
Read more about that on the [compiling docs](docs/compiling.md).
### nix package ### nix package
If you have Nix package manager installed, you can use: If you have Nix package manager installed, you can use:
@ -82,16 +86,21 @@ messages starting with a slash are sent to chat only if the `--disable-commands`
## docs ## docs
- [Message formats](docs/message_formats.md) - [Compiling](docs/compiling.md)
- [User agents](docs/user_agents.md)
- [Using as crate](docs/crate.md)
- [Authenticated mode](docs/auth_mode.md) - [Authenticated mode](docs/auth_mode.md)
- [Cross compile](docs/cross_compile.md) - [WRAC protocol (v2.0)](docs/wrac.md)
- [About RAC URL](docs/url.md)
- [FAQ](docs/faq.md) - [FAQ](docs/faq.md)
## see also ## see also
- [RAC-Hub - all about RAC protocol](https://the-stratosphere-solutions.github.io/RAC-Hub/) - [Racinfo - webpage with info about RAC](https://racinfo.kostyazero.com/)
- [RAC-Hub - all about RAC protocol](https://meexreay.github.io/RAC-Hub/)
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol) - [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) - [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
- [Mefidroniy - client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client) - [Mefidroniy - TUI client for RAC](https://github.com/OctoBanon-Main/mefedroniy-client)
- [cRACk - client for RAC kettles](https://github.com/pansangg/cRACk)
- [AlmatyD - server for RACv1.0](https://gitea.bedohswe.eu.org/bedohswe/almatyd) - [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) - [RAC protocol (v1.0)](https://bedohswe.eu.org/text/rac/protocol.md.html)

View File

@ -1,16 +1,12 @@
use { use std::io;
std::{
env,
io,
},
winresource::WindowsResource,
};
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
#[cfg(feature = "winapi")]
{
use {std::env, winresource::WindowsResource};
if env::var_os("CARGO_CFG_WINDOWS").is_some() { if env::var_os("CARGO_CFG_WINDOWS").is_some() {
WindowsResource::new() WindowsResource::new().set_icon("misc/icon.ico").compile()?;
.set_icon("misc/icon.ico") }
.compile()?;
} }
Ok(()) Ok(())
} }

85
docs/compiling.md Normal file
View File

@ -0,0 +1,85 @@
# How to compile it
## Windows
1. Install [rustup](https://rustup.rs/)
2. Install [MSVC](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and run `rustup default stable-msvc`
3. Extract [GTK4 from gvsbuild](https://github.com/wingtk/gvsbuild/releases/latest) to `C:\gtk`
4. Update environment variables:
- Go to Start, search for 'Advanced system settings' (or click on Properties of My Computer in the Explorer, then you'll find 'Advanced system settings')
- Click 'Environment Variables...'
- Add `C:\gtk\lib\pkgconfig` to the PKG_CONFIG_PATH variable (or create one if doesnt exist)
- Add `C:\gtk\bin` to the PATH variable (or create one if doesnt exist)
- Add `C:\gtk\lib` to the Lib variable (or create one if doesnt exist)
- Apply and close the window (maybe restart PC)
5. Open the repository directory in console (download it from github or with `git clone https://github.com/MeexReay/bRAC.git`)
6. Run `cargo build -r -F winapi,notify-rust`
7. Done! Your finished binary is in the `target/release` folder.
## Linux / MacOS
1. Install `rust`, `openssl-dev`, `gtk4-dev` with your package manager
2. Open the repository directory in console (download it from github or with `git clone https://github.com/MeexReay/bRAC.git`)
3. Run `cargo build -r`
4. Done! Your finished binary is in the `target/release` folder.
# Troubleshooting
## Windows
### Black frame around the window
Black frame appears on connecting to the server or when bRAC just freezes. Be patient.
## MacOS
### Notifications dont work
There are two solutions:
- Switch to `libnotify`:
Add the feature `libnotify` to cargo: `cargo build -r -F libnotify`
- Switch to `notify-rust`:
Add the feature `notify-rust` to cargo: `cargo build -r -F notify-rust`
## Linux
### Notifications dont work
There are two solutions:
- Switch to `libnotify`:
Just add the new feature to cargo: `cargo build -r -F libnotify` \
Libnotify sucks in many situations, but it always work
- Make a desktop file:
Enter the repository folder and run: `./misc/create-desktop.sh` \
You'll get a desktop file contents, just edit paths here and write it to a new file in the `~/.local/share/applications` or `/usr/share/applications`\
All of these, with adding icons and other, makes this command: `make install` (using `gnumake` package) \
But make sure, that you have `.local/bin` in the `PATH` variable, otherwise it won't work. \
Now, if you'll run with the desktop file, GNotifications will work perfectly.
# Cross-compiling
## From Linux to Windows
```bash
./misc/build.sh
```
## From NixOS to Windows
```bash
nix-shell -p rustup gcc cargo-cross zip unzip curl
rustup toolchain install stable
./misc/build.sh
```
## From Windows to Linux
That's your problem

65
docs/crate.md Normal file
View File

@ -0,0 +1,65 @@
# Using as crate
This article describes how to use the client as rust crate
## Installation
To use exact version:
```toml
[dependencies.bRAC]
git = "https://github.com/MeexReay/bRAC"
tag = "0.1.2+2.0"
default-features = false
```
To use with latest changes:
```toml
[dependencies.bRAC]
git = "https://github.com/MeexReay/bRAC"
default-features = false
```
`default-features = false` here removes GTK4 gui from installation.
## Usage
As the code structure was changed like about gazillion times,
you need to explore it yourself, if you are using an old version.
Here is example of usage on commit [80e7b8c](https://github.com/MeexReay/bRAC/commit/80e7b8c50642f9b76be06980305ed03253858d0c)
```rust
use bRAC::proto::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut conn = connect("wracs://meex.lol", None)?;
// read docs/url.md
// this keep-alive way with only one connection
// works only for WRAC, for a regular RAC,
// you have to connect to the server on each request
send_message(&mut conn, "<dude> hi RAC-loving kikes!")?;
register_user(&mut conn, "dude", "password")?;
send_message_auth(&mut conn, "dude", "password", "my auth message")?;
send_message_spoof_auth(&mut conn, "<dude> this message totally fucks auth system")?;
let (mut all_messages, last_size) = read_messages(&mut conn, 10, 0, false)?.unwrap(); // limits with 10 messages
/* imagine that new messages were written here */
let (mut new_messages, last_size) = read_messages(&mut conn, 10, last_size, true)?.unwrap(); // chunked reading!
all_messages.append(&mut new_messages);
println!("all_messages: {all_messages:?}. last_size: {last_size}");
Ok(())
}
```
## See more
- [rac-rs - A Rust client library for RAC protocol. (with async support)](https://github.com/kostya-zero/rac-rs)

View File

@ -1,15 +0,0 @@
# 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
```

View File

@ -1,47 +0,0 @@
# 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}`)

16
docs/url.md Normal file
View File

@ -0,0 +1,16 @@
# How does RAC URL work?
RAC URL is used in sRAC and bRAC as the default way of specifying host, running a RAC or WRAC server.
Format of RAC URL:
```
<protocol>://<address>[:<port>]
```
Protocol can be one of these:
| | **SSL** | **No SSL** |
| :--: | :--: | :--: |
| **WebSocket** | wracs:// | wrac:// |
| **No Websocket** | racs:// | rac:// |

20
docs/user_agents.md Normal file
View File

@ -0,0 +1,20 @@
# user agents
User agents in RAC is the way how to get know from what client the message was sent. It works by just checking the message text throught regex.
## clients
Here are listed the most common clients, and their name colors in the chat.
| Client | Format | Regex | Color |
| :----: | :----: | :----: | :----: |
| [bRAC](https://github.com/MeexReay/bRAC) | 리㹰<{name}> {text} | `\uB9AC\u3E70<(.*?)> (.*)` | green
| [CRAB](https://gitea.bedohswe.eu.org/pixtaded/crab) | ═══<{name}> {text} | `\u2550\u2550\u2550<(.*?)> (.*)` | light red
| [Mefidroniy](https://github.com/OctoBanon-Main/mefedroniy-client) | °ʘ<{name}> {text} | `\u00B0\u0298<(.*?)> (.*)` | light magenta
| [cRACk](https://github.com/pansangg/cRACk) | ⁂<{name}> {text} | `\u2042<(.*?)> (.*)` | gold
| [Snowdrop](https://github.com/Forbirdden/Snowdrop) | ඞ<{name}> {text} | `\u0D9E<(.*?)> (.*)` | light green
| clRAC | <{name}> {text} | `<(.*?)> (.*)` | cyan
## developer notes
in auth-mode, there is must to be `> ` after name (`{name}> {text}`)

84
docs/wrac.md Normal file
View File

@ -0,0 +1,84 @@
# WRACv2.0 Protocol
Uses websocket for connections, and sends binary data only (works in packet-way manner)
Totally inherits all packets of RACv2, except of reading messages
## Sending messages
Client sends:
- Byte `0x01`
- Message text
## Sending authorized messages
Client sends:
- Byte `0x02`
- Username
- `\n`
- Password
- `\n`
- Message
Server sends:
- nothing if message was sent successfully
- `0x01` if the user does not exists
- `0x02` if the password is incorrect
## Registration users
Client sends:
- Byte `0x03`
- Username
- `\n`
- Password
Server sends:
- nothing if user was registered successfully
- `0x01` if the username is already taken
## Reading messages
### Getting message length
Client sends:
- Byte `0x00`
Server sends:
- Size of all messages in ASCII (data_size)
### Normal reading
This packet is independent from getting message length packet.
Client sends:
- Byte `0x00`
- Byte `0x01`
Server sends:
- All messages
### Chunked reading
This packet is independent from getting message length packet.
Client sends:
- Byte `0x00`
- Byte `0x02`
- Size of messages you have in ASCII (last_size)
Server sends:
- All new messages
*for example: if you want to read last N bytes, last_size = data_size - N*

23
examples/protocol.rs Normal file
View File

@ -0,0 +1,23 @@
use bRAC::proto::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut conn = connect("rac://meex.lol", None)?;
send_message(&mut conn, "<dude> hi RAC-loving kikes!")?;
register_user(&mut conn, "dude", "password")?;
send_message_auth(&mut conn, "dude", "password", "my auth message")?;
send_message_spoof_auth(&mut conn, "<dude> this message totally fucks auth system")?;
let (mut all_messages, last_size) = read_messages(&mut conn, 10, 0, false)?.unwrap(); // limits with 10 messages
/* imagine that new messages were written here */
let (mut new_messages, last_size) = read_messages(&mut conn, 10, last_size, true)?.unwrap(); // chunked reading!
all_messages.append(&mut new_messages);
println!("all_messages: {all_messages:?}. last_size: {last_size}");
Ok(())
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 30 KiB

46
misc/build.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
echo "Run this script only from repository root!"
echo "This script depends on:"
echo " - fact that you are on linux x86_64!"
echo " - zip, unzip, curl. install it with your distro's package manager"
echo " - cross crate. to install it, run this: cargo install cross --git https://github.com/cross-rs/cross"
echo " - docker, so you should run something like this on your distro: sudo systemctl start docker"
read -p "Press enter if you really want to do rm -rf build/"
rm -rf build
mkdir build
build_linux() {
mkdir build/linux-x86_64
mkdir build/linux-x86_64/misc
cargo build -r
cp target/release/bRAC build/linux-x86_64/misc/bRAC-gnotif
cp misc/user-install.sh build/linux-x86_64/install.sh
cp misc/bRAC.png build/linux-x86_64/misc
cp misc/create-desktop.sh build/linux-x86_64/misc
cargo build -r -F libnotify
cp target/release/bRAC build/linux-x86_64
cp README.md build/linux-x86_64
cp LICENSE build/linux-x86_64
zip -r build/bRAC-linux-x86_64.zip build/linux-x86_64
}
build_windows() {
chmod +x misc/mslink.sh
curl -L https://github.com/wingtk/gvsbuild/releases/download/2025.5.0/GTK4_Gvsbuild_2025.5.0_x64.zip -o build/gvsbuild.zip # TODO: make this link auto-update
unzip build/gvsbuild.zip "bin/*" -d build/windows-x86_64
rm build/gvsbuild.zip
cross build --target x86_64-pc-windows-gnu -F notify-rust,winapi -r
cp target/x86_64-pc-windows-gnu/release/bRAC.exe build/windows-x86_64/bin
echo "@echo off" > build/windows-x86_64/start.bat
echo "set \"PATH=%CD%\bin;%PATH%\"" >> build/windows-x86_64/start.bat
echo "start \"\" /B \"bin\bRAC.exe\"" >> build/windows-x86_64/start.bat
./misc/mslink.sh -l "%COMSPEC% /C start start.bat" -o build/windows-x86_64/bRAC.lnk # TODO: fix this lnk
cp README.md build/windows-x86_64
curl https://raw.githubusercontent.com/wingtk/gvsbuild/refs/heads/main/COPYING -o build/windows-x86_64/LICENSE
zip -r build/bRAC-windows-x86_64.zip build/windows-x86_64
}
build_linux
build_windows

15
misc/create-desktop.sh Normal file
View File

@ -0,0 +1,15 @@
#!/bin/bash
version=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"(.*)"/\1/')
echo "[Desktop Entry]"
echo "Name=bRAC"
echo "Version=$version"
echo "Type=Application"
echo "Comment=better RAC client"
echo "Icon=$HOME/.local/share/bRAC/icon.png"
echo "Exec=$HOME/.local/bin/bRAC"
echo "Categories=Network;"
echo "StartupNotify=true"
echo "Terminal=false"
echo "X-GNOME-UsesNotifications=true"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 20 KiB

243
misc/mslink.sh Executable file
View File

@ -0,0 +1,243 @@
#!/bin/bash
# mslink: Allow to create Windows Shortcut without the need of Windows
#
# Copyright (C) 2019 Mikaël Le Bohec
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
#############################################################################################
# mslink.sh v1.3
#############################################################################################
# Ce script permet de créer un Raccourci Windows (Fichier .LNK)
# Script créé en se basant sur la doc
# http://msdn.microsoft.com/en-us/library/dd871305.aspx
#############################################################################################
OPTIONS=$(getopt -q -n ${0} -o hpl:o:n:w:a:i: -l help,lnk-target:,output-file:,name:,working-dir:,arguments:,icon:,printer-link -- "$@")
eval set -- ${OPTIONS}
IS_PRINTER_LNK=0
while true; do
case "$1" in
-h|--help) HELP=1 ;;
-p|--printer-link) IS_PRINTER_LNK=1 ;;
-l|--lnk-target) LNK_TARGET="$2" ; shift ;;
-o|--output-file) OUTPUT_FILE="$2" ; shift ;;
-n|--name) param_HasName="$2" ; shift ;;
-w|--working-dir) param_HasWorkingDir="$2" ; shift ;;
-a|--arguments) param_HasArguments="$2" ; shift ;;
-i|--icon) param_HasIconLocation="$2" ; shift ;;
--) shift ; break ;;
*) echo "Option inconnue : $1" ; exit 1 ;;
esac
shift
done
if [ $# -ne 0 ]; then
echo "Option(s) inconnue(s) : $@"
exit 1
fi
[ ${#LNK_TARGET} -eq 0 ] || [ ${#OUTPUT_FILE} -eq 0 ] && echo "
Usage :
${0} -l cible_du_fichier_lnk [-n description] [-w working_dir] [-a cmd_args] [-i icon_path] -o mon_fichier.lnk [-p]
Options :
-l, --lnk-target Précise la cible du raccourci
-o, --output-file Enregistre le raccourci dans un fichier
-n, --name Spécifie une description au raccourci
-w, --working-dir Spécifie le répertoire de lancement de la commande
-a, --arguments Spécifie les arguments de la commande lancée
-i, --icon Spécifie le chemin de l'icône
-p, --printer-link Génère un raccourci de type imprimante réseau
" && exit 1
#############################################################################################
# Fonctions
#############################################################################################
function ascii2hex() {
echo $(echo -n ${1} | hexdump -v -e '/1 " x%02x"'|sed s/\ /\\\\/g)
}
function gen_LinkFlags() {
echo '\x'$(printf '%02x' "$((HasLinkTargetIDList + HasName + HasWorkingDir + HasArguments + HasIconLocation))")${LinkFlags_2_3_4}
}
function gen_Data_string() {
ITEM_SIZE=$(printf '%04x' $((${#1})))
echo '\x'${ITEM_SIZE:2:2}'\x'${ITEM_SIZE:0:2}$(ascii2hex ${1})
}
function gen_IDLIST() {
ITEM_SIZE=$(printf '%04x' $((${#1}/4+2)))
echo '\x'${ITEM_SIZE:2:2}'\x'${ITEM_SIZE:0:2}${1}
}
function convert_CLSID_to_DATA() {
echo -n ${1:6:2}${1:4:2}${1:2:2}${1:0:2}${1:11:2}${1:9:2}${1:16:2}${1:14:2}${1:19:4}${1:24:12}|sed s/"\([A-Fa-f0-9][A-Fa-f0-9]\)"/\\\\x\\1/g
}
#############################################################################################
# Variables issues de la documentation officielle de Microsoft
#############################################################################################
HasLinkTargetIDList=0x01
HasName=0x04
HasWorkingDir=0x10
HasArguments=0x20
HasIconLocation=0x40
HeaderSize='\x4c\x00\x00\x00' # HeaderSize
LinkCLSID=$(convert_CLSID_to_DATA "00021401-0000-0000-c000-000000000046") # LinkCLSID
LinkFlags_2_3_4='\x01\x00\x00' # ForceNoLinkInfo
LinkFlags=""
FileAttributes_Directory='\x10\x00\x00\x00' # FILE_ATTRIBUTE_DIRECTORY
FileAttributes_File='\x20\x00\x00\x00' # FILE_ATTRIBUTE_ARCHIVE
CreationTime='\x00\x00\x00\x00\x00\x00\x00\x00'
AccessTime='\x00\x00\x00\x00\x00\x00\x00\x00'
WriteTime='\x00\x00\x00\x00\x00\x00\x00\x00'
FileSize='\x00\x00\x00\x00'
IconIndex='\x00\x00\x00\x00'
ShowCommand='\x01\x00\x00\x00' # SW_SHOWNORMAL
Hotkey='\x00\x00' # No Hotkey
Reserved='\x00\x00' # Valeur non modifiable
Reserved2='\x00\x00\x00\x00' # Valeur non modifiable
Reserved3='\x00\x00\x00\x00' # Valeur non modifiable
TerminalID='\x00\x00' # Valeur non modifiable
CLSID_Computer="20d04fe0-3aea-1069-a2d8-08002b30309d" # Poste de travail
CLSID_Network="208d2c60-3aea-1069-a2d7-08002b30309d" # Favoris réseau
#############################################################################################
# Constantes trouvées à partir de l'analyse de fichiers lnk
#############################################################################################
PREFIX_LOCAL_ROOT='\x2f' # Disque local
PREFIX_FOLDER='\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # Dossier de fichiers
PREFIX_FILE='\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # Fichier
PREFIX_NETWORK_ROOT='\xc3\x01\x81' # Racine de serveur de fichiers réseau
PREFIX_NETWORK_PRINTER='\xc3\x02\xc1' # Imprimante réseau
END_OF_STRING='\x00'
#############################################################################################
if [ ! -z "${param_HasName}" ]; then
STRING_DATA=${STRING_DATA}$(gen_Data_string ${param_HasName})
else
HasName=0x00
fi
if [ ! -z "${param_HasWorkingDir}" ]; then
STRING_DATA=${STRING_DATA}$(gen_Data_string ${param_HasWorkingDir})
else
HasWorkingDir=0x00
fi
if [ ! -z "${param_HasArguments}" ]; then
STRING_DATA=${STRING_DATA}$(gen_Data_string ${param_HasArguments})
else
HasArguments=0x00
fi
if [ ! -z "${param_HasIconLocation}" ]; then
STRING_DATA=${STRING_DATA}$(gen_Data_string ${param_HasIconLocation})
else
HasIconLocation=0x00
fi
LinkFlags=$(gen_LinkFlags)
# On retire l'anti-slash final s'il y en a un
LNK_TARGET=${LNK_TARGET%\\}
# On sépare le chemin racine du lien de la cible finale
# On distingue aussi si le lien est de type local ou réseau
# On définie la valeur Item_Data suivant le cas d'un lien réseau ou local
IS_ROOT_LNK=0
IS_NETWORK_LNK=0
if [[ ${LNK_TARGET} == \\\\* ]]; then
IS_NETWORK_LNK=1
PREFIX_ROOT=${PREFIX_NETWORK_ROOT}
Item_Data='\x1f\x58'$(convert_CLSID_to_DATA ${CLSID_Network})
TARGET_ROOT=${LNK_TARGET%\\*}
if [[ ${LNK_TARGET} == \\\\*\\* ]]; then
TARGET_LEAF=${LNK_TARGET##*\\}
fi
if [ ${TARGET_ROOT} == \\ ]; then
TARGET_ROOT=${LNK_TARGET}
fi
else
PREFIX_ROOT=${PREFIX_LOCAL_ROOT}
Item_Data='\x1f\x50'$(convert_CLSID_to_DATA ${CLSID_Computer})
TARGET_ROOT=${LNK_TARGET%%\\*}
if [[ ${LNK_TARGET} == *\\* ]]; then
TARGET_LEAF=${LNK_TARGET#*\\}
fi
[[ ! ${TARGET_ROOT} == *\\ ]] && TARGET_ROOT=${TARGET_ROOT}'\'
fi
if [ ${IS_PRINTER_LNK} -eq 1 ]; then
PREFIX_ROOT=${PREFIX_NETWORK_PRINTER}
TARGET_ROOT=${LNK_TARGET}
IS_ROOT_LNK=1
fi
[ ${#TARGET_LEAF} -eq 0 ] && IS_ROOT_LNK=1
#############################################################################################
# On sélectionne le préfixe qui sera utilisé pour afficher l'icône du raccourci
if [[ ${TARGET_LEAF} == *.??? ]]; then
PREFIX_OF_TARGET=${PREFIX_FILE}
TYPE_TARGET="fichier"
FileAttributes=${FileAttributes_File}
else
PREFIX_OF_TARGET=${PREFIX_FOLDER}
TYPE_TARGET="dossier"
FileAttributes=${FileAttributes_Directory}
fi
# On convertit les valeurs des cibles en binaire
TARGET_ROOT=$(ascii2hex "${TARGET_ROOT}")
TARGET_ROOT=${TARGET_ROOT}$(for i in `seq 1 21`;do echo -n '\x00';done) # Nécessaire à partir de Vista et supérieur sinon le lien est considéré comme vide (je n'ai trouvé nul part d'informations à ce sujet)
TARGET_LEAF=$(ascii2hex "${TARGET_LEAF}")
# On crée l'IDLIST qui représente le cœur du fichier LNK
if [ ${IS_ROOT_LNK} -eq 1 ];then
IDLIST_ITEMS=$(gen_IDLIST ${Item_Data})$(gen_IDLIST ${PREFIX_ROOT}${TARGET_ROOT}${END_OF_STRING})
else
IDLIST_ITEMS=$(gen_IDLIST ${Item_Data})$(gen_IDLIST ${PREFIX_ROOT}${TARGET_ROOT}${END_OF_STRING})$(gen_IDLIST ${PREFIX_OF_TARGET}${TARGET_LEAF}${END_OF_STRING})
fi
IDLIST=$(gen_IDLIST ${IDLIST_ITEMS})
#############################################################################################
if [ ${IS_NETWORK_LNK} -eq 1 ]; then
TYPE_LNK="réseau"
if [ ${IS_PRINTER_LNK} -eq 1 ]; then
TYPE_TARGET="imprimante"
fi
else
TYPE_LNK="local"
fi
echo "Création d'un raccourci de type \""${TYPE_TARGET}" "${TYPE_LNK}"\" avec pour cible "${LNK_TARGET} ${param_HasArguments} 1>&2
echo -ne ${HeaderSize}${LinkCLSID}${LinkFlags}${FileAttributes}${CreationTime}${AccessTime}${WriteTime}${FileSize}${IconIndex}${ShowCommand}${Hotkey}${Reserved}${Reserved2}${Reserved3}${IDLIST}${TerminalID}${STRING_DATA} > "${OUTPUT_FILE}"

View File

@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
echo "this script is deprecated, fix it yourself if you wanna to"; exit
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" echo "This script must be run as root"
exit 1 exit 1

View File

@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
echo "this script is deprecated, fix it yourself if you wanna to"; exit
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" echo "This script must be run as root"
exit 1 exit 1

View File

@ -1,7 +1,12 @@
#!/bin/bash #!/bin/bash
cp bRAC ~/.local/bin/bRAC mkdir -p ~/.local
mkdir -p ~/.local/bin
mkdir -p ~/.local/share
mkdir -p ~/.local/share/bRAC
cp misc/bRAC-gnotif ~/.local/bin/bRAC
chmod +x ~/.local/bin/bRAC chmod +x ~/.local/bin/bRAC
mkdir ~/.local/share/bRAC -p
cp misc/bRAC.png ~/.local/share/bRAC/icon.png cp misc/bRAC.png ~/.local/share/bRAC/icon.png
cp misc/bRAC.desktop ~/.local/share/applications/ru.themixray.bRAC.desktop ./misc/create-desktop.sh > ~/.local/share/applications/ru.themixray.bRAC.desktop

View File

@ -1,74 +1,88 @@
use std::str::FromStr;
use std::{fs, path::PathBuf};
use serde_yml;
use serde_default::DefaultFromSerde;
use clap::Parser; use clap::Parser;
use serde_default::DefaultFromSerde;
use serde_yml;
use std::{fs, path::PathBuf};
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}"; const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
fn default_true() -> bool { true } fn default_true() -> bool {
pub fn default_max_messages() -> usize { 200 } true
pub fn default_update_time() -> usize { 50 } }
pub fn default_host() -> String { "meex.lol:11234".to_string() } pub fn default_max_messages() -> usize {
pub fn default_message_format() -> String { MESSAGE_FORMAT.to_string() } 200
}
pub fn default_update_time() -> usize {
100
}
pub fn default_oof_update_time() -> usize {
10000
}
pub fn default_konata_size() -> usize {
100
}
pub fn default_host() -> String {
"wracs://meex.lol:11234".to_string()
}
pub fn default_message_format() -> String {
MESSAGE_FORMAT.to_string()
}
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)] #[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
pub struct Config { pub struct Config {
#[serde(default = "default_host")] pub host: String, #[serde(default = "default_host")]
#[serde(default)] pub name: Option<String>, pub host: String,
#[serde(default = "default_message_format")] pub message_format: String, #[serde(default)]
#[serde(default = "default_update_time")] pub update_time: usize, pub name: Option<String>,
#[serde(default = "default_max_messages")] pub max_messages: usize, #[serde(default = "default_message_format")]
#[serde(default = "default_true")] pub hide_my_ip: bool, pub message_format: String,
#[serde(default)] pub show_other_ip: bool, #[serde(default = "default_update_time")]
#[serde(default)] pub auth_enabled: bool, pub update_time: usize,
#[serde(default)] pub ssl_enabled: bool, #[serde(default = "default_oof_update_time")]
#[serde(default = "default_true")] pub chunked_enabled: bool, pub oof_update_time: usize,
#[serde(default = "default_true")] pub formatting_enabled: bool, #[serde(default = "default_max_messages")]
#[serde(default = "default_true")] pub commands_enabled: bool, pub max_messages: usize,
#[serde(default)] pub wrac_enabled: bool, #[serde(default = "default_konata_size")]
#[serde(default)] pub proxy: Option<String>, pub konata_size: usize,
#[serde(default = "default_true")] pub notifications_enabled: bool, #[serde(default)]
pub remove_gui_shit: bool,
#[serde(default = "default_true")]
pub hide_my_ip: bool,
#[serde(default)]
pub show_other_ip: 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 proxy: Option<String>,
#[serde(default = "default_true")]
pub notifications_enabled: bool,
#[serde(default)]
pub debug_logs: bool,
} }
#[cfg(target_os = "windows")]
pub fn get_config_path() -> PathBuf { 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; use std::env;
use std::str::FromStr;
env::var("APPDATA") env::var("APPDATA")
.ok() .ok()
.and_then(|o| Some(PathBuf::from_str(&o).ok()?.join("bRAC"))) .and_then(|o| Some(PathBuf::from_str(&o).ok()?.join("bRAC")))
} { .unwrap_or("bRAC/config.yml".into())
config_dir = dir; }
}
config_dir.join("config.yml") #[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn get_config_path() -> PathBuf {
use homedir::my_home;
my_home()
.ok()
.flatten()
.map(|o| o.join(".config"))
.map(|o| o.join("bRAC"))
.unwrap_or("bRAC".into())
.join("config.yml")
} }
pub fn load_config(path: PathBuf) -> Config { pub fn load_config(path: PathBuf) -> Config {
@ -94,50 +108,100 @@ pub fn save_config(path: PathBuf, config: &Config) {
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Args { pub struct Args {
/// Print config path /// Print config path
#[arg(short='p', long)] #[arg(short = 'p', long)]
pub config_path: bool, pub config_path: bool,
/// Print unformatted messages from chat and exit /// Print unformatted messages from chat and exit
#[arg(short='r', long)] #[arg(short = 'r', long)]
pub read_messages: bool, pub read_messages: bool,
/// Send unformatted message to chat and exit /// Send unformatted message to chat and exit
#[arg(short='s', long, value_name="MESSAGE")] #[arg(short = 's', long, value_name = "MESSAGE")]
pub send_message: Option<String>, pub send_message: Option<String>,
#[arg(short='H', long)] pub host: Option<String>, #[arg(short = 'H', long)]
#[arg(short='n', long)] pub name: Option<String>, pub host: Option<String>,
#[arg(long)] pub message_format: Option<String>, #[arg(short = 'n', long)]
#[arg(long)] pub update_time: Option<usize>, pub name: Option<String>,
#[arg(long)] pub max_messages: Option<usize>, #[arg(long)]
#[arg(long)] pub hide_my_ip: Option<bool>, pub message_format: Option<String>,
#[arg(long)] pub show_other_ip: Option<bool>, #[arg(long)]
#[arg(long)] pub auth_enabled:Option <bool>, pub update_time: Option<usize>,
#[arg(long)] pub ssl_enabled: Option<bool>, #[arg(long)]
#[arg(long)] pub chunked_enabled: Option<bool>, pub oof_update_time: Option<usize>,
#[arg(long)] pub formatting_enabled: Option<bool>, #[arg(long)]
#[arg(long)] pub commands_enabled: Option<bool>, pub max_messages: Option<usize>,
#[arg(long)] pub notifications_enabled: Option<bool>, #[arg(long)]
#[arg(long)] pub wrac_enabled: Option<bool>, pub konata_size: Option<usize>,
#[arg(long)] pub proxy: Option<String>, #[arg(long)]
pub hide_my_ip: Option<bool>,
#[arg(long)]
pub show_other_ip: Option<bool>,
#[arg(long)]
pub remove_gui_shit: 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 proxy: Option<String>,
#[arg(long)]
pub debug_logs: bool,
} }
impl Args { impl Args {
pub fn patch_config(&self, config: &mut Config) { pub fn patch_config(&self, config: &mut Config) {
if let Some(v) = self.host.clone() { config.host = v } if let Some(v) = self.host.clone() {
if let Some(v) = self.name.clone() { config.name = Some(v) } config.host = 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.name.clone() {
if let Some(v) = self.update_time { config.update_time = v } config.name = Some(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.proxy.clone() {
if let Some(v) = self.show_other_ip { config.show_other_ip = v } config.proxy = Some(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.message_format.clone() {
if let Some(v) = self.chunked_enabled { config.chunked_enabled = v } config.message_format = 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.update_time {
if let Some(v) = self.notifications_enabled { config.notifications_enabled = v } config.update_time = v
if let Some(v) = self.wrac_enabled { config.wrac_enabled = v } }
if let Some(v) = self.oof_update_time {
config.oof_update_time = v
}
if let Some(v) = self.max_messages {
config.max_messages = v
}
if let Some(v) = self.konata_size {
config.konata_size = 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.remove_gui_shit {
config.remove_gui_shit = 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 self.debug_logs {
config.debug_logs = true
}
} }
} }

View File

@ -1,4 +1,8 @@
use std::sync::{atomic::{AtomicUsize, Ordering}, mpsc::Sender, Arc, RwLock}; use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc::Sender,
Arc, RwLock,
};
use rand::random; use rand::random;
@ -7,10 +11,11 @@ use super::config::Config;
pub struct Context { pub struct Context {
pub registered: RwLock<Option<String>>, pub registered: RwLock<Option<String>>,
pub config: RwLock<Config>, pub config: RwLock<Config>,
pub sender: RwLock<Option<Arc<Sender<(String, bool)>>>>, pub sender: RwLock<Option<Arc<Sender<(Vec<String>, bool)>>>>,
pub messages: RwLock<Vec<String>>, pub messages: RwLock<Vec<String>>,
pub packet_size: AtomicUsize, pub packet_size: AtomicUsize,
pub name: RwLock<String> pub name: RwLock<String>,
pub is_focused: AtomicBool,
} }
impl Context { impl Context {
@ -21,7 +26,13 @@ impl Context {
sender: RwLock::new(None), sender: RwLock::new(None),
messages: RwLock::new(Vec::new()), messages: RwLock::new(Vec::new()),
packet_size: AtomicUsize::default(), packet_size: AtomicUsize::default(),
name: RwLock::new(config.name.clone().unwrap_or_else(|| format!("Anon#{:X}", random::<u16>()))), name: RwLock::new(
config
.name
.clone()
.unwrap_or_else(|| format!("Anon#{:X}", random::<u16>())),
),
is_focused: AtomicBool::new(true),
} }
} }
@ -31,13 +42,16 @@ impl Context {
pub fn set_config(&self, config: &Config) { pub fn set_config(&self, config: &Config) {
*self.config.write().unwrap() = config.clone(); *self.config.write().unwrap() = config.clone();
*self.name.write().unwrap() = config.name.clone().unwrap_or_else(|| format!("Anon#{:X}", random::<u16>())); *self.name.write().unwrap() = config
.name
.clone()
.unwrap_or_else(|| format!("Anon#{:X}", random::<u16>()));
*self.registered.write().unwrap() = None; *self.registered.write().unwrap() = None;
*self.messages.write().unwrap() = Vec::new(); *self.messages.write().unwrap() = Vec::new();
self.packet_size.store(0, Ordering::SeqCst); self.packet_size.store(0, Ordering::SeqCst);
} }
pub fn config<T>(&self, map: fn (&Config) -> T) -> T { pub fn config<T>(&self, map: fn(&Config) -> T) -> T {
map(&self.config.read().unwrap()) map(&self.config.read().unwrap())
} }
@ -49,7 +63,12 @@ impl Context {
self.messages.read().unwrap().clone() self.messages.read().unwrap().clone()
} }
pub fn put_messages_packet(&self, max_length: usize, messages: Vec<String>, packet_size: usize) { pub fn put_messages_packet(
&self,
max_length: usize,
messages: Vec<String>,
packet_size: usize,
) {
self.packet_size.store(packet_size, Ordering::SeqCst); self.packet_size.store(packet_size, Ordering::SeqCst);
let mut messages = messages; let mut messages = messages;
if messages.len() > max_length { if messages.len() > max_length {
@ -58,7 +77,12 @@ impl Context {
*self.messages.write().unwrap() = messages; *self.messages.write().unwrap() = messages;
} }
pub fn add_messages_packet(&self, max_length: usize, messages: Vec<String>, packet_size: usize) { pub fn add_messages_packet(
&self,
max_length: usize,
messages: Vec<String>,
packet_size: usize,
) {
self.packet_size.store(packet_size, Ordering::SeqCst); self.packet_size.store(packet_size, Ordering::SeqCst);
self.add_message(max_length, messages); self.add_message(max_length, messages);
} }
@ -76,9 +100,7 @@ macro_rules! connect_rac {
($ctx:ident) => { ($ctx:ident) => {
&mut connect( &mut connect(
&$ctx.config(|o| o.host.clone()), &$ctx.config(|o| o.host.clone()),
$ctx.config(|o| o.ssl_enabled),
$ctx.config(|o| o.proxy.clone()), $ctx.config(|o| o.proxy.clone()),
$ctx.config(|o| o.wrac_enabled)
)? )?
}; };
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,19 +1,26 @@
use std::{ use std::{
error::Error, sync::Arc, thread, time::{Duration, SystemTime, UNIX_EPOCH} error::Error,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
}; };
use crate::connect_rac; use crate::connect_rac;
use super::proto::{connect, read_messages, send_message, send_message_spoof_auth, register_user, send_message_auth}; use super::proto::{
connect, read_messages, register_user, send_message, send_message_auth,
};
use gui::{add_chat_message, clear_chat_messages};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use ctx::Context; use ctx::Context;
#[cfg(feature = "gtk")]
pub mod gui;
#[cfg(feature = "gtk")]
pub use gui::run_main_loop; pub use gui::run_main_loop;
#[cfg(feature = "gtk")]
use gui::{add_chat_messages, clear_chat_messages};
const HELP_MESSAGE: &str = "Help message: const HELP_MESSAGE: &str = "Help message:
/help - show help message /help - show help message
@ -31,21 +38,22 @@ lazy_static! {
pub static ref IP_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![ 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{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), "#70fa7a".to_string()), // bRAC
(Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), "red".to_string()), // CRAB (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), "#fa7070".to_string()), // CRAB
(Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), "magenta".to_string()), // Mefidroniy (Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), "#da70fa".to_string()), // Mefidroniy
(Regex::new(r"<(.*?)> (.*)").unwrap(), "cyan".to_string()), // clRAC (Regex::new(r"\u{2042}<(.*?)> (.*)").unwrap(), "#f8b91b".to_string()), // cRACk
(Regex::new(r"\u{0D9E}<(.*?)> (.*)").unwrap(), "#aeff00".to_string()), // Snowdrop
(Regex::new(r"<(.*?)> (.*)").unwrap(), "#70fadc".to_string()), // clRAC
]; ];
pub static ref SERVER_LIST: Vec<String> = vec![ pub static ref SERVER_LIST: Vec<String> = vec![
"wracs://meex.lol:11234".to_string(),
"rac://meex.lol".to_string(), "rac://meex.lol".to_string(),
"rac://meex.lol:11234".to_string(), "wracs://meex.lol".to_string(),
"rac://91.192.22.20".to_string() "rac://91.192.22.20".to_string()
]; ];
} }
pub mod gui;
pub mod config; pub mod config;
pub mod ctx; pub mod ctx;
@ -55,59 +63,67 @@ pub fn sanitize_text(input: &str) -> String {
cleaned_text.into_owned() cleaned_text.into_owned()
} }
#[cfg(feature = "gtk")]
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> { pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
for i in message.split("\n") for i in message.split("\n").map(|o| o.to_string()) {
.map(|o| o.to_string()) {
print_message(ctx.clone(), i)?; print_message(ctx.clone(), i)?;
} }
Ok(()) Ok(())
} }
#[cfg(feature = "gtk")]
pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> { pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> {
let command = command.trim_start_matches("/"); let command = command.trim_start_matches("/");
let (command, args) = command.split_once(" ").unwrap_or((&command, "")); let (command, args) = command.split_once(" ").unwrap_or((&command, ""));
let args = args.split(" ").collect::<Vec<&str>>(); let args = args.split(" ").collect::<Vec<&str>>();
if command == "clear" { if command == "clear" {
let Some(times) = args.get(0) else { return Ok(()) }; let Some(times) = args.get(0) else {
return Ok(());
};
let times = times.parse()?; let times = times.parse()?;
for _ in 0..times { for _ in 0..times {
send_message(connect_rac!(ctx), "\r")?; send_message(connect_rac!(ctx), "\r")?;
} }
} else if command == "spam" { } else if command == "spam" {
let Some(times) = args.get(0) else { return Ok(()) }; let Some(times) = args.get(0) else {
return Ok(());
};
let times = times.parse()?; let times = times.parse()?;
let msg = args[1..].join(" "); let msg = args[1..].join(" ");
for _ in 0..times { for _ in 0..times {
send_message(connect_rac!(ctx), &("\r".to_string()+&msg))?; send_message(connect_rac!(ctx), &("\r".to_string() + &msg))?;
} }
} else if command == "help" { } else if command == "help" {
add_message(ctx.clone(), HELP_MESSAGE)?; add_message(ctx.clone(), HELP_MESSAGE)?;
} else if command == "register" { } else if command == "register" {
let Some(pass) = args.get(0) else { let Some(pass) = args.get(0) else {
add_message(ctx.clone(), "please provide password as the first argument")?; add_message(ctx.clone(), "please provide password as the first argument")?;
return Ok(()) return Ok(());
}; };
match register_user(connect_rac!(ctx), &ctx.name(), pass, !ctx.config(|o| o.ssl_enabled)) { match register_user(connect_rac!(ctx), &ctx.name(), pass) {
Ok(true) => { Ok(true) => {
add_message(ctx.clone(), "you was registered successfully bro")?; add_message(ctx.clone(), "you was registered successfully bro")?;
*ctx.registered.write().unwrap() = Some(pass.to_string()); *ctx.registered.write().unwrap() = Some(pass.to_string());
}, }
Ok(false) => add_message(ctx.clone(), "user with this account already exists bruh")?, Ok(false) => add_message(ctx.clone(), "user with this account already exists bruh")?,
Err(e) => add_message(ctx.clone(), &format!("ERROR while registrationing: {}", e))? Err(e) => add_message(ctx.clone(), &format!("ERROR while registrationing: {}", e))?,
}; };
} else if command == "login" { } else if command == "login" {
let Some(pass) = args.get(0) else { let Some(pass) = args.get(0) else {
add_message(ctx.clone(), "please provide password as the first argument")?; add_message(ctx.clone(), "please provide password as the first argument")?;
return Ok(()) return Ok(());
}; };
add_message(ctx.clone(), "ye bro you was logged in")?; add_message(ctx.clone(), "ye bro you was logged in")?;
*ctx.registered.write().unwrap() = Some(pass.to_string()); *ctx.registered.write().unwrap() = Some(pass.to_string());
} else if command == "ping" { } else if command == "ping" {
let mut before = ctx.packet_size(); let mut before = ctx.packet_size();
let message = format!("Checking ping... {:X}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()); let message = format!(
"Checking ping... {:X}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
);
send_message(connect_rac!(ctx), &message)?; send_message(connect_rac!(ctx), &message)?;
@ -118,9 +134,10 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
connect_rac!(ctx), connect_rac!(ctx),
ctx.config(|o| o.max_messages), ctx.config(|o| o.max_messages),
before, before,
!ctx.config(|o| o.ssl_enabled), ctx.config(|o| o.chunked_enabled),
ctx.config(|o| o.chunked_enabled) )
).ok().flatten(); .ok()
.flatten();
if let Some((data, size)) = data { if let Some((data, size)) = data {
if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) { if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) {
@ -135,7 +152,10 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
} }
} }
add_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; add_message(
ctx.clone(),
&format!("Ping = {}ms", start.elapsed().unwrap().as_millis()),
)?;
} else { } else {
add_message(ctx.clone(), "Unknown command bruh")?; add_message(ctx.clone(), "Unknown command bruh")?;
} }
@ -144,7 +164,8 @@ pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>
} }
pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String { pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String {
format!("{}{}{}", format!(
"{}{}{}",
if ctx.config(|o| o.hide_my_ip) { if ctx.config(|o| o.hide_my_ip) {
"\r\x07" "\r\x07"
} else { } else {
@ -152,14 +173,8 @@ pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String {
}, },
message, message,
if !ctx.config(|o| o.hide_my_ip) { if !ctx.config(|o| o.hide_my_ip) {
let spaces = if ctx.config(|o| o.auth_enabled) { if message.chars().count() < 54 {
39 " ".repeat(54 - message.chars().count())
} else {
54
};
if message.chars().count() < spaces {
" ".repeat(spaces-message.chars().count())
} else { } else {
String::new() String::new()
} }
@ -169,12 +184,14 @@ pub fn prepare_message(ctx: Arc<Context>, message: &str) -> String {
) )
} }
#[cfg(feature = "gtk")]
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> { 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()]); ctx.add_message(ctx.config(|o| o.max_messages), vec![message.clone()]);
add_chat_message(ctx.clone(), message); add_chat_messages(ctx.clone(), vec![message]);
Ok(()) Ok(())
} }
#[cfg(feature = "gtk")]
pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> { pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
let last_size = ctx.packet_size(); let last_size = ctx.packet_size();
@ -182,47 +199,36 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
connect_rac!(ctx), connect_rac!(ctx),
ctx.config(|o| o.max_messages), ctx.config(|o| o.max_messages),
ctx.packet_size(), ctx.packet_size(),
!ctx.config(|o| o.ssl_enabled), ctx.config(|o| o.chunked_enabled),
ctx.config(|o| o.chunked_enabled)
) { ) {
Ok(Some((messages, size))) => { Ok(Some((messages, size))) => {
if ctx.config(|o| o.chunked_enabled) { if ctx.config(|o| o.chunked_enabled) {
ctx.add_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size); ctx.add_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
if last_size == 0 { if last_size == 0 {
if messages.len() >= 1 { clear_chat_messages(ctx.clone(), messages);
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 { } else {
for msg in messages { add_chat_messages(ctx.clone(), messages);
add_chat_message(ctx.clone(), msg.clone());
}
} }
} else { } else {
ctx.put_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size); ctx.put_messages_packet(ctx.config(|o| o.max_messages), messages.clone(), size);
if messages.len() >= 1 { clear_chat_messages(ctx.clone(), messages);
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) => { Err(e) => {
println!("Read messages error: {}", e.to_string()) if ctx.config(|o| o.debug_logs) {
add_chat_messages(
ctx.clone(),
vec![format!("Read messages error: {}", e.to_string())],
);
}
} }
_ => {} _ => {}
} }
thread::sleep(Duration::from_millis(ctx.config(|o| o.update_time) as u64));
Ok(()) Ok(())
} }
#[cfg(feature = "gtk")]
pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> { pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
if message.starts_with("/") && ctx.config(|o| o.commands_enabled) { if message.starts_with("/") && ctx.config(|o| o.commands_enabled) {
on_command(ctx.clone(), &message)?; on_command(ctx.clone(), &message)?;
@ -231,13 +237,11 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
ctx.clone(), ctx.clone(),
&ctx.config(|o| o.message_format.clone()) &ctx.config(|o| o.message_format.clone())
.replace("{name}", &ctx.name()) .replace("{name}", &ctx.name())
.replace("{text}", &message) .replace("{text}", &message),
); );
if let Some(password) = ctx.registered.read().unwrap().clone() { if let Some(password) = ctx.registered.read().unwrap().clone() {
send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message, !ctx.config(|o| o.ssl_enabled))?; send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message)?;
} else if ctx.config(|o| o.auth_enabled) {
send_message_spoof_auth(connect_rac!(ctx), &message, !ctx.config(|o| o.ssl_enabled))?;
} else { } else {
send_message(connect_rac!(ctx), &message)?; send_message(connect_rac!(ctx), &message)?;
} }
@ -248,16 +252,16 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
pub fn sanitize_message(message: String) -> Option<String> { pub fn sanitize_message(message: String) -> Option<String> {
let message = sanitize_text(&message); let message = sanitize_text(&message);
let message = message.trim().to_string(); let message = message.trim().to_string();
Some(message) Some(message)
} }
/// message -> (date, ip, text, (name, color)) /// message -> (date, ip, text, (name, color))
pub fn parse_message(message: String) -> Option<(String, Option<String>, String, Option<(String, String)>)> { pub fn parse_message(
message: String,
) -> Option<(String, Option<String>, String, Option<(String, String)>)> {
if message.is_empty() { if message.is_empty() {
return None return None;
} }
let date = DATE_REGEX.captures(&message)?; let date = DATE_REGEX.captures(&message)?;
@ -274,7 +278,10 @@ pub fn parse_message(message: String) -> Option<(String, Option<String>, String,
.to_string(); .to_string();
let (ip, message) = if let Some(message) = IP_REGEX.captures(&message) { 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()) (
Some(message.get(1)?.as_str().to_string()),
message.get(2)?.as_str().to_string(),
)
} else { } else {
(None, message) (None, message)
}; };
@ -291,7 +298,11 @@ pub fn parse_message(message: String) -> Option<(String, Option<String>, String,
pub fn find_username_color(message: &str) -> Option<(String, String, String)> { pub fn find_username_color(message: &str) -> Option<(String, String, String)> {
for (re, color) in COLORED_USERNAMES.iter() { for (re, color) in COLORED_USERNAMES.iter() {
if let Some(captures) = re.captures(message) { if let Some(captures) = re.captures(message) {
return Some((captures[1].to_string(), captures[2].to_string(), color.clone())) return Some((
captures[1].to_string(),
captures[2].to_string(),
color.clone(),
));
} }
} }
None None

View File

@ -1,3 +1,6 @@
.message-content { color:rgb(255, 255, 255); }
/* Now made with GTK Pango Markup */
/* .message-content { color:rgb(255, 255, 255); }
.message-date { color:rgb(146, 146, 146); } .message-date { color:rgb(146, 146, 146); }
.message-ip { color:rgb(73, 73, 73); } .message-ip { color:rgb(73, 73, 73); } */

View File

@ -1,3 +1,6 @@
.message-content { color:rgb(0, 0, 0); }
/* Now made with GTK Pango Markup */
/* .message-content { color:rgb(0, 0, 0); }
.message-date { color:rgb(41, 41, 41); } .message-date { color:rgb(41, 41, 41); }
.message-ip { color:rgb(88, 88, 88); } .message-ip { color:rgb(88, 88, 88); } */

View File

@ -1,5 +1,3 @@
.send-button, .send-text { border-radius: 0; } .send-button, .send-text { border-radius: 0; }
.calendar { .calendar {
transform: scale(0.6); transform: scale(0.6);
@ -16,9 +14,11 @@
font-weight: bold; font-weight: bold;
} }
.message-name { font-weight: bold; } /* Now made with GTK Pango Markup */
/* .message-name { font-weight: bold; }
.message-name-green { color: #70fa7a; } .message-name-green { color: #70fa7a; }
.message-name-red { color: #fa7070; } .message-name-red { color: #fa7070; }
.message-name-magenta { color: #da70fa; } .message-name-magenta { color: #da70fa; }
.message-name-cyan { color: #70fadc; } .message-name-cyan { color: #70fadc; } */

View File

@ -1,12 +1,17 @@
use std::sync::Arc; use std::sync::Arc;
use bRAC::chat::{
config::{get_config_path, load_config, Args},
ctx::Context,
};
use bRAC::proto::{connect, read_messages, send_message}; 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; use clap::Parser;
fn main() { fn main() {
#[cfg(feature = "winapi")] #[cfg(feature = "winapi")]
unsafe { winapi::um::wincon::FreeConsole() }; unsafe {
winapi::um::wincon::FreeConsole()
};
let args = Args::parse(); let args = Args::parse();
@ -19,37 +24,39 @@ fn main() {
let mut config = load_config(config_path); let mut config = load_config(config_path);
if args.read_messages { args.patch_config(&mut config);
let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message");
print!("{}", read_messages( if args.read_messages {
&mut stream, let mut stream =
config.max_messages, connect(&config.host, config.proxy.clone()).expect("Error reading message");
0,
!config.ssl_enabled, print!(
false "{}",
) read_messages(&mut stream, config.max_messages, 0, false)
.ok().flatten() .ok()
.expect("Error reading messages").0.join("\n") .flatten()
.expect("Error reading messages")
.0
.join("\n")
); );
} }
if let Some(message) = &args.send_message { 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"); let mut stream =
connect(&config.host, config.proxy.clone()).expect("Error sending message");
send_message( send_message(&mut stream, message).expect("Error sending 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); #[cfg(feature = "gtk")]
{
let ctx = Arc::new(Context::new(&config)); let ctx = Arc::new(Context::new(&config));
use bRAC::chat::run_main_loop;
run_main_loop(ctx.clone()); run_main_loop(ctx.clone());
}
} }

View File

@ -1,8 +1,14 @@
use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, time::Duration}; use std::{
error::Error,
fmt::Debug,
io::{Read, Write},
net::{TcpStream, ToSocketAddrs},
time::Duration,
};
use native_tls::{TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use socks::Socks5Stream; use socks::Socks5Stream;
use tungstenite::WebSocket; use tungstenite::{client::client_with_config, protocol::WebSocketConfig, WebSocket};
pub mod rac; pub mod rac;
pub mod wrac; pub mod wrac;
@ -13,28 +19,44 @@ pub trait Stream: Read + Write + Unpin + Send + Sync + Debug {
} }
impl Stream for TcpStream { impl Stream for TcpStream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); } 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 { impl Stream for Socks5Stream {
fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout)); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout)); } 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> { impl<T: Stream> Stream for TlsStream<T> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } 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>> { impl Stream for TlsStream<Box<dyn Stream>> {
fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_read_timeout(&self, timeout: Duration) {
fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } self.get_ref().set_read_timeout(timeout);
}
fn set_write_timeout(&self, timeout: Duration) {
self.get_ref().set_write_timeout(timeout);
}
} }
pub enum RacStream { pub enum RacStream {
WRAC(WebSocket<Box<dyn Stream>>), WRAC(WebSocket<Box<dyn Stream>>),
RAC(Box<dyn Stream>) RAC(Box<dyn Stream>),
} }
/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \ /// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \
@ -64,46 +86,42 @@ pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
let (scheme, url) = url.split_once("://").unwrap_or(("rac", url)); let (scheme, url) = url.split_once("://").unwrap_or(("rac", url));
let (host, _) = url.split_once("/").unwrap_or((url, "")); let (host, _) = url.split_once("/").unwrap_or((url, ""));
match scheme.to_lowercase().as_str() { match scheme.to_lowercase().as_str() {
"rac" => { "rac" => Some((
Some((
if host.contains(":") { if host.contains(":") {
host.to_string() host.to_string()
} else { } else {
format!("{host}:42666") format!("{host}:42666")
}, },
false, false false,
)) false,
}, )),
"racs" => { "racs" => Some((
Some((
if host.contains(":") { if host.contains(":") {
host.to_string() host.to_string()
} else { } else {
format!("{host}:42667") format!("{host}:42667")
}, },
true, false true,
)) false,
}, )),
"wrac" => { "wrac" => Some((
Some((
if host.contains(":") { if host.contains(":") {
host.to_string() host.to_string()
} else { } else {
format!("{host}:52666") format!("{host}:52666")
}, },
false, true false,
)) true,
}, )),
"wracs" => { "wracs" => Some((
Some((
if host.contains(":") { if host.contains(":") {
host.to_string() host.to_string()
} else { } else {
format!("{host}:52667") format!("{host}:52667")
}, },
true, true true,
)) true,
}, )),
_ => None, _ => None,
} }
} }
@ -114,14 +132,19 @@ pub fn parse_rac_url(url: &str) -> Option<(String, bool, bool)> {
/// ssl - wrap with ssl client, write false if you dont know what it is /// ssl - wrap with ssl client, write false if you dont know what it is
/// proxy - socks5 proxy (host, (user, pass)) /// proxy - socks5 proxy (host, (user, pass))
/// wrac - to use wrac protocol /// wrac - to use wrac protocol
pub fn connect(host: &str, ssl: bool, proxy: Option<String>, wrac: bool) -> Result<RacStream, Box<dyn Error>> { pub fn connect(host: &str, proxy: Option<String>) -> Result<RacStream, Box<dyn Error>> {
let (host, ssl_, wrac_) = parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?; let (host, ssl, wrac) =
let (ssl, wrac) = (ssl_ || ssl, wrac_ || wrac); parse_rac_url(host).ok_or::<Box<dyn Error>>("url parse error".into())?;
let stream: Box<dyn Stream> = if let Some(proxy) = proxy { let stream: Box<dyn Stream> = if let Some(proxy) = proxy {
if let Some((proxy, auth)) = parse_socks5_url(&proxy) { if let Some((proxy, auth)) = parse_socks5_url(&proxy) {
if let Some((user, pass)) = auth { if let Some((user, pass)) = auth {
Box::new(Socks5Stream::connect_with_password(&proxy, host.as_str(), &user, &pass)?) Box::new(Socks5Stream::connect_with_password(
&proxy,
host.as_str(),
&user,
&pass,
)?)
} else { } else {
Box::new(Socks5Stream::connect(&proxy, host.as_str())?) Box::new(Socks5Stream::connect(&proxy, host.as_str())?)
} }
@ -129,32 +152,39 @@ pub fn connect(host: &str, ssl: bool, proxy: Option<String>, wrac: bool) -> Resu
return Err("proxy parse error".into()); return Err("proxy parse error".into());
} }
} else { } else {
let addr = host.to_socket_addrs()?.next().ok_or::<Box<dyn Error>>("addr parse error".into())?; let addr = host
.to_socket_addrs()?
.next()
.ok_or::<Box<dyn Error>>("addr parse error".into())?;
Box::new(TcpStream::connect(&addr)?) Box::new(TcpStream::connect(&addr)?)
}; };
let stream = if ssl { let stream = if ssl {
let ip: String = host.split_once(":") let ip: String = host
.split_once(":")
.map(|o| o.0.to_string()) .map(|o| o.0.to_string())
.unwrap_or(host.clone()); .unwrap_or(host.clone());
Box::new(TlsConnector::builder() Box::new(
TlsConnector::builder()
.danger_accept_invalid_certs(true) .danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true) .danger_accept_invalid_hostnames(true)
.build()? .build()?
.connect(&ip, stream)?) .connect(&ip, stream)?,
)
} else { } else {
stream stream
}; };
stream.set_read_timeout(Duration::from_secs(3)); stream.set_read_timeout(Duration::from_secs(15)); // TODO: softcode this
stream.set_write_timeout(Duration::from_secs(3)); stream.set_write_timeout(Duration::from_secs(15));
if wrac { if wrac {
let (client, _) = tungstenite::client( let (client, _) = client_with_config(
&format!("ws{}://{host}", if ssl { "s" } else { "" }), &format!("ws://{host}"),
stream stream,
Some(WebSocketConfig::default().max_message_size(Some(512 * 1024 * 1024))), // TODO: softcode this
)?; )?;
Ok(RacStream::WRAC(client)) Ok(RacStream::WRAC(client))
} else { } else {
@ -162,42 +192,14 @@ pub fn connect(host: &str, ssl: bool, proxy: Option<String>, wrac: bool) -> Resu
} }
} }
/// 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 /// Send message
/// ///
/// stream - any stream that can be written to /// stream - any stream that can be written to
/// message - message text /// message - message text
pub fn send_message( pub fn send_message(stream: &mut RacStream, message: &str) -> Result<(), Box<dyn Error>> {
stream: &mut RacStream,
message: &str
) -> Result<(), Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::send_message(websocket, message), RacStream::WRAC(websocket) => wrac::send_message(websocket, message),
RacStream::RAC(stream) => rac::send_message(stream, message) RacStream::RAC(stream) => rac::send_message(stream, message),
} }
} }
@ -213,11 +215,10 @@ pub fn register_user(
stream: &mut RacStream, stream: &mut RacStream,
name: &str, name: &str,
password: &str, password: &str,
remove_null: bool
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password), RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password),
RacStream::RAC(stream) => rac::register_user(stream, name, password, remove_null) RacStream::RAC(stream) => rac::register_user(stream, name, password),
} }
} }
@ -237,11 +238,10 @@ pub fn send_message_auth(
name: &str, name: &str,
password: &str, password: &str,
message: &str, message: &str,
remove_null: bool
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message), RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message),
RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message, remove_null) RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message),
} }
} }
@ -257,11 +257,12 @@ pub fn read_messages(
stream: &mut RacStream, stream: &mut RacStream,
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
remove_null: bool, chunked: bool,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
match stream { match stream {
RacStream::WRAC(websocket) => wrac::read_messages(websocket, max_messages, last_size, chunked), RacStream::WRAC(websocket) => {
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, remove_null, chunked) wrac::read_messages(websocket, max_messages, last_size, chunked)
}
RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, chunked),
} }
} }

View File

@ -1,4 +1,7 @@
use std::{error::Error, io::{Read, Write}}; use std::{
error::Error,
io::{Read, Write},
};
/// Send message /// Send message
/// ///
@ -21,23 +24,13 @@ pub fn register_user(
stream: &mut (impl Write + Read), stream: &mut (impl Write + Read),
name: &str, name: &str,
password: &str, password: &str,
remove_null: bool
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
if remove_null {
if let Ok(out) = skip_null(stream) { if let Ok(out) = skip_null(stream) {
Ok(out[0] == 0) Ok(out[0] == 0)
} else { } else {
Ok(true) 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 /// Send message with auth
@ -56,24 +49,13 @@ pub fn send_message_auth(
name: &str, name: &str,
password: &str, password: &str,
message: &str, message: &str,
remove_null: bool
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?; stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?;
if remove_null {
if let Ok(out) = skip_null(stream) { if let Ok(out) = skip_null(stream) {
Ok(out[0]) Ok(out[0])
} else { } else {
Ok(0) 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 /// Skip null bytes and return first non-null byte
@ -82,7 +64,7 @@ pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
let mut buf = vec![0; 1]; let mut buf = vec![0; 1];
stream.read_exact(&mut buf)?; stream.read_exact(&mut buf)?;
if buf[0] != 0 { if buf[0] != 0 {
break Ok(buf) break Ok(buf);
} }
} }
} }
@ -90,7 +72,7 @@ pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
/// remove trailing null bytes in vector /// remove trailing null bytes in vector
pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> { pub fn remove_trailing_null(vec: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
while vec.ends_with(&[0]) { while vec.ends_with(&[0]) {
vec.remove(vec.len()-1); vec.remove(vec.len() - 1);
} }
Ok(()) Ok(())
} }
@ -107,26 +89,17 @@ pub fn read_messages(
stream: &mut (impl Read + Write), stream: &mut (impl Read + Write),
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
remove_null: bool, chunked: bool,
chunked: bool
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write_all(&[0x00])?; stream.write_all(&[0x00])?;
let packet_size = { let packet_size = {
let data = if remove_null {
let mut data = skip_null(stream)?; let mut data = skip_null(stream)?;
let mut buf = vec![0; 10]; let mut buf = vec![0; 10];
let len = stream.read(&mut buf)?; let len = stream.read(&mut buf)?;
buf.truncate(len); buf.truncate(len);
data.append(&mut buf); data.append(&mut buf);
remove_trailing_null(&mut data)?; 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)? String::from_utf8(data)?
.trim_matches(char::from(0)) .trim_matches(char::from(0))
@ -145,23 +118,22 @@ pub fn read_messages(
packet_size - last_size packet_size - last_size
}; };
let packet_data = if remove_null { let mut packet_data = skip_null(stream)?;
let mut data = skip_null(stream)?;
let mut buf = vec![0; to_read - 1]; let mut buf = vec![0; to_read - 1];
stream.read_exact(&mut buf)?; stream.read_exact(&mut buf)?;
data.append(&mut buf); packet_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 packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect(); let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter() let lines: Vec<String> = lines
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) .clone()
.into_iter()
.skip(if lines.len() >= max_messages {
lines.len() - max_messages
} else {
0
})
.map(|o| o.to_string()) .map(|o| o.to_string())
.collect(); .collect();

View File

@ -1,6 +1,8 @@
use std::{error::Error, io::{Read, Write}}; use std::{
use tungstenite::{WebSocket, Message}; error::Error,
io::{Read, Write},
};
use tungstenite::{Message, WebSocket};
/// Send message /// Send message
/// ///
@ -8,9 +10,11 @@ use tungstenite::{WebSocket, Message};
/// message - message text /// message - message text
pub fn send_message( pub fn send_message(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
message: &str message: &str,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
stream.write(Message::Binary(format!("\x01{message}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x01{message}").as_bytes().to_vec().into(),
))?;
stream.flush()?; stream.flush()?;
Ok(()) Ok(())
} }
@ -25,9 +29,11 @@ pub fn send_message(
pub fn register_user( pub fn register_user(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
name: &str, name: &str,
password: &str password: &str,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x03{name}\n{password}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x03{name}\n{password}").as_bytes().to_vec().into(),
))?;
stream.flush()?; stream.flush()?;
if let Ok(msg) = stream.read() { if let Ok(msg) = stream.read() {
Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0) Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0)
@ -50,9 +56,14 @@ pub fn send_message_auth(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
name: &str, name: &str,
password: &str, password: &str,
message: &str message: &str,
) -> Result<u8, Box<dyn Error>> { ) -> Result<u8, Box<dyn Error>> {
stream.write(Message::Binary(format!("\x02{name}\n{password}\n{message}").as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x02{name}\n{password}\n{message}")
.as_bytes()
.to_vec()
.into(),
))?;
stream.flush()?; stream.flush()?;
if let Ok(msg) = stream.read() { if let Ok(msg) = stream.read() {
if msg.is_binary() { if msg.is_binary() {
@ -76,7 +87,7 @@ pub fn read_messages(
stream: &mut WebSocket<impl Write + Read>, stream: &mut WebSocket<impl Write + Read>,
max_messages: usize, max_messages: usize,
last_size: usize, last_size: usize,
chunked: bool chunked: bool,
) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> { ) -> Result<Option<(Vec<String>, usize)>, Box<dyn Error>> {
stream.write(Message::Binary(vec![0x00].into()))?; stream.write(Message::Binary(vec![0x00].into()))?;
stream.flush()?; stream.flush()?;
@ -101,7 +112,9 @@ pub fn read_messages(
stream.write(Message::Binary(vec![0x00, 0x01].into()))?; stream.write(Message::Binary(vec![0x00, 0x01].into()))?;
packet_size packet_size
} else { } else {
stream.write(Message::Binary(format!("\x00\x02{}", last_size).as_bytes().to_vec().into()))?; stream.write(Message::Binary(
format!("\x00\x02{}", last_size).as_bytes().to_vec().into(),
))?;
packet_size - last_size packet_size - last_size
}; };
stream.flush()?; stream.flush()?;
@ -119,8 +132,14 @@ pub fn read_messages(
let packet_data = String::from_utf8_lossy(&packet_data).to_string(); let packet_data = String::from_utf8_lossy(&packet_data).to_string();
let lines: Vec<&str> = packet_data.split("\n").collect(); let lines: Vec<&str> = packet_data.split("\n").collect();
let lines: Vec<String> = lines.clone().into_iter() let lines: Vec<String> = lines
.skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) .clone()
.into_iter()
.skip(if lines.len() >= max_messages {
lines.len() - max_messages
} else {
0
})
.map(|o| o.to_string()) .map(|o| o.to_string())
.collect(); .collect();