WIP: hellfire #1
18
config.toml
18
config.toml
@ -1,14 +1,26 @@
|
|||||||
server = "irc.supernets.org"
|
#[server]
|
||||||
|
server = "198.98.52.138" #"irc.supernets.org"
|
||||||
port = 6697
|
port = 6697
|
||||||
use_ssl = true
|
use_ssl = true
|
||||||
|
|
||||||
|
#[user]
|
||||||
nickname = "g1r"
|
nickname = "g1r"
|
||||||
channel = "#superbowl"
|
realname = "git.supernets.org/sad/g1r"
|
||||||
|
channels = ["#dev", "#superbowl", "#5000"]
|
||||||
sasl_username = ""
|
sasl_username = ""
|
||||||
sasl_password = ""
|
sasl_password = ""
|
||||||
capabilities = ["sasl"]
|
capabilities = ["sasl"]
|
||||||
|
|
||||||
|
#[proxy]
|
||||||
use_proxy = false
|
use_proxy = false
|
||||||
proxy_type = "socks5"
|
proxy_type = "socks5"
|
||||||
proxy_addr = "127.0.0.1"
|
proxy_addr = "127.0.0.1"
|
||||||
proxy_port = 9050
|
proxy_port = 1080
|
||||||
proxy_username = ""
|
proxy_username = ""
|
||||||
proxy_password = ""
|
proxy_password = ""
|
||||||
|
|
||||||
|
#[features]
|
||||||
|
kickrejoin = true
|
||||||
|
ascii_art = "./ircart/ircart"
|
||||||
|
pump_delay = 0 # in milliseconds
|
||||||
|
|
||||||
|
67
src/main.rs
67
src/main.rs
@ -1,10 +1,11 @@
|
|||||||
use tokio::io::{split, AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{split, AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, BufReader, AsyncBufReadExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio_native_tls::native_tls::TlsConnector as NTlsConnector;
|
use tokio_native_tls::native_tls::TlsConnector as NTlsConnector;
|
||||||
use tokio_native_tls::TlsConnector;
|
use tokio_native_tls::TlsConnector;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use tokio_socks::tcp::Socks5Stream;
|
use tokio_socks::tcp::Socks5Stream;
|
||||||
@ -15,7 +16,8 @@ struct Config {
|
|||||||
port: u16,
|
port: u16,
|
||||||
use_ssl: bool,
|
use_ssl: bool,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
channel: String,
|
realname: Option<String>,
|
||||||
|
channels: Vec<String>,
|
||||||
sasl_username: Option<String>,
|
sasl_username: Option<String>,
|
||||||
sasl_password: Option<String>,
|
sasl_password: Option<String>,
|
||||||
capabilities: Option<Vec<String>>,
|
capabilities: Option<Vec<String>>,
|
||||||
@ -27,15 +29,19 @@ struct Config {
|
|||||||
proxy_port: Option<u16>,
|
proxy_port: Option<u16>,
|
||||||
proxy_username: Option<String>,
|
proxy_username: Option<String>,
|
||||||
proxy_password: Option<String>,
|
proxy_password: Option<String>,
|
||||||
|
|
||||||
|
ascii_art: Option<String>,
|
||||||
|
pump_delay: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod mods {
|
mod mods {
|
||||||
pub mod sasl;
|
pub mod sasl;
|
||||||
pub mod sed;
|
pub mod sed;
|
||||||
|
pub mod ascii;
|
||||||
}
|
}
|
||||||
use mods::sasl::{start_sasl_auth, handle_sasl_messages};
|
use mods::sasl::{start_sasl_auth, handle_sasl_messages};
|
||||||
use mods::sed::{SedCommand, MessageBuffer};
|
use mods::sed::{SedCommand, MessageBuffer};
|
||||||
|
use mods::ascii::handle_ascii_command;
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread", worker_threads = 12)]
|
#[tokio::main(flavor = "multi_thread", worker_threads = 12)]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -106,10 +112,9 @@ async fn proxy_exec(config: &Config) -> Result<TcpStream, Box<dyn std::error::Er
|
|||||||
let proxy_stream = TcpStream::connect(proxy).await.unwrap();
|
let proxy_stream = TcpStream::connect(proxy).await.unwrap();
|
||||||
let username = config.proxy_username.clone().unwrap();
|
let username = config.proxy_username.clone().unwrap();
|
||||||
let password = config.proxy_password.clone().unwrap();
|
let password = config.proxy_password.clone().unwrap();
|
||||||
let tcp_stream = if !&username.is_empty() && !password.is_empty() {
|
let mut tcp_stream = if !&username.is_empty() && !password.is_empty() {
|
||||||
let tcp_stream = Socks5Stream::connect_with_password_and_socket(proxy_stream, server, &username, &password).await.unwrap();
|
let tcp_stream =Socks5Stream::connect_with_password_and_socket(proxy_stream, server, &username, &password).await.unwrap();
|
||||||
tcp_stream
|
tcp_stream
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let tcp_stream = Socks5Stream::connect_with_socket(proxy_stream, server).await.unwrap();
|
let tcp_stream = Socks5Stream::connect_with_socket(proxy_stream, server).await.unwrap();
|
||||||
tcp_stream
|
tcp_stream
|
||||||
@ -156,8 +161,6 @@ async fn readmsg(mut reader: tokio::io::ReadHalf<tokio_native_tls::TlsStream<Tcp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static SASL_AUTH: AtomicBool = AtomicBool::new(false);
|
static SASL_AUTH: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Write messages to the server
|
/// Write messages to the server
|
||||||
@ -166,14 +169,15 @@ async fn writemsg(mut writer: tokio::io::WriteHalf<tokio_native_tls::TlsStream<T
|
|||||||
let username = config.sasl_username.clone().unwrap();
|
let username = config.sasl_username.clone().unwrap();
|
||||||
let password = config.sasl_password.clone().unwrap();
|
let password = config.sasl_password.clone().unwrap();
|
||||||
let nickname = config.nickname.clone();
|
let nickname = config.nickname.clone();
|
||||||
|
let realname = config.realname.clone().unwrap_or(nickname.clone());
|
||||||
if !password.is_empty() && !SASL_AUTH.load(Ordering::Relaxed) {
|
if !password.is_empty() && !SASL_AUTH.load(Ordering::Relaxed) {
|
||||||
let capabilities = config.capabilities.clone();
|
let capabilities = config.capabilities.clone();
|
||||||
println!("Starting SASL auth...");
|
println!("Starting SASL auth...");
|
||||||
start_sasl_auth(&mut writer, "PLAIN", &nickname, capabilities).await.unwrap();
|
start_sasl_auth(&mut writer, "PLAIN", &nickname, &realname, capabilities).await.unwrap();
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
SASL_AUTH.store(true, Ordering::Relaxed);
|
SASL_AUTH.store(true, Ordering::Relaxed);
|
||||||
} else {
|
} else {
|
||||||
nickme(&mut writer, &nickname).await.unwrap();
|
nickme(&mut writer, &nickname, &realname).await.unwrap();
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,36 +210,59 @@ async fn writemsg(mut writer: tokio::io::WriteHalf<tokio_native_tls::TlsStream<T
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *cmd == "376" {
|
if *cmd == "376" {
|
||||||
println!("Joining channel");
|
println!("Joining channels");
|
||||||
writer.write_all(format!("JOIN {}\r\n", config.channel).as_bytes()).await.unwrap();
|
for channel in &config.channels {
|
||||||
|
writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap();
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if *cmd == "KICK" {
|
||||||
|
let channel = parts[2];
|
||||||
|
let userme = parts[3];
|
||||||
|
if userme == nickname {
|
||||||
|
writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap();
|
||||||
|
writer.flush().await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
if *cmd == "PRIVMSG" {
|
if *cmd == "PRIVMSG" {
|
||||||
let channel = parts[2];
|
let channel = parts[2];
|
||||||
let user = parts[0].strip_prefix(':').unwrap().split_at(parts[0].find('!').unwrap()).0.strip_suffix('!').unwrap();
|
let user = parts[0].strip_prefix(':')
|
||||||
let host = parts[0].split_at(parts[0].find('!').unwrap()).1;
|
.and_then(|user_with_host| user_with_host.split('!').next())
|
||||||
|
.unwrap_or("unknown_user");
|
||||||
|
let host = parts[0].split('@').nth(1).unwrap_or("unknown_host");
|
||||||
let msg_content = parts[3..].join(" ").replace(':', "");
|
let msg_content = parts[3..].join(" ").replace(':', "");
|
||||||
println!("{} {} {} {} {} {} {}", "DEBUG:".bold().yellow(), "channel:".bold().green(), channel.purple(), "user:".bold().green(), host.purple(), "msg:".bold().green(), msg_content.purple());
|
println!("{} {} {} {} {} {} {} {} {}", "DEBUG:".bold().yellow(), "channel:".bold().green(), channel.purple(), "user:".bold().green(), user.purple(), "host:".bold().green(), host.purple(), "msg:".bold().green(), msg_content.yellow());
|
||||||
|
|
||||||
// sed
|
// sed
|
||||||
if msg_content.starts_with("s/") {
|
if msg_content.starts_with("s/") {
|
||||||
println!("Sed command detected");
|
if let Some(sed_command) = SedCommand::parse(&msg_content.clone()) {
|
||||||
if let Some(sed_command) = SedCommand::parse(&msg_content) {
|
|
||||||
if let Some(response) = message_buffer.apply_sed_command(&sed_command) {
|
if let Some(response) = message_buffer.apply_sed_command(&sed_command) {
|
||||||
writer.write_all(format!("PRIVMSG {} :{}: {}\r\n", channel, user, response).as_bytes()).await.unwrap();
|
writer.write_all(format!("PRIVMSG {} :{}: {}\r\n", channel, user, response).as_bytes()).await.unwrap();
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message_buffer.add_message(msg_content);
|
message_buffer.add_message(msg_content.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ansi art
|
||||||
|
//
|
||||||
|
if msg_content.starts_with("%ascii") {
|
||||||
|
handle_ascii_command(&mut writer, &config, &msg_content, channel).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// other commands here
|
// other commands here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn nickme<W: tokio::io::AsyncWriteExt + Unpin>(writer: &mut W, nickname: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
|
|
||||||
|
|
||||||
|
async fn nickme<W: tokio::io::AsyncWriteExt + Unpin>(writer: &mut W, nickname: &str, realname: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
writer.write_all(format!("NICK {}\r\n", nickname).as_bytes()).await?;
|
writer.write_all(format!("NICK {}\r\n", nickname).as_bytes()).await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
writer.write_all(format!("USER {} 0 * :{}\r\n", nickname, nickname).as_bytes()).await?;
|
writer.write_all(format!("USER {} 0 * :{}\r\n", nickname, realname).as_bytes()).await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
157
src/mods/ascii.rs
Normal file
157
src/mods/ascii.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use tokio::io::{AsyncWriteExt, BufReader};
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::time::{self, Duration};
|
||||||
|
use std::fs;
|
||||||
|
use rand::Rng;
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
use std::error::Error;
|
||||||
|
use crate::Config;
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 4096;
|
||||||
|
|
||||||
|
async fn send_ansi_art<W: AsyncWriteExt + Unpin>(writer: &mut W, file_path: &str, pump_delay: u64, channel: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let file = File::open(file_path).await?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut lines = reader.lines();
|
||||||
|
|
||||||
|
let mut line_count = 0;
|
||||||
|
let lines_stream = &mut lines;
|
||||||
|
while let Ok(Some(_)) = lines_stream.next_line().await {
|
||||||
|
line_count += 1;
|
||||||
|
}
|
||||||
|
let mut pump_delay = Duration::from_millis(pump_delay);
|
||||||
|
if line_count > 500 && pump_delay < Duration::from_millis(100){
|
||||||
|
pump_delay = Duration::from_millis(100);
|
||||||
|
}
|
||||||
|
let file = File::open(file_path).await?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut lines = reader.lines();
|
||||||
|
|
||||||
|
|
||||||
|
while let Some(line) = lines.next_line().await? {
|
||||||
|
|
||||||
|
if line.len() > CHUNK_SIZE {
|
||||||
|
for chunk in line.as_bytes().chunks(CHUNK_SIZE) {
|
||||||
|
writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, String::from_utf8_lossy(chunk)).as_bytes()).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
time::sleep(pump_delay).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, line).as_bytes()).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
time::sleep(pump_delay).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_random_file(dir: &str) -> Option<String> {
|
||||||
|
let files = fs::read_dir(dir).ok()?.filter_map(|entry| {
|
||||||
|
let path = entry.ok()?.path();
|
||||||
|
if path.is_file() {
|
||||||
|
path.to_str().map(ToString::to_string)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).collect::<Vec<String>>();
|
||||||
|
|
||||||
|
if files.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let index = rng.gen_range(0..files.len());
|
||||||
|
files.get(index).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_ascii_command<W: AsyncWriteExt + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
config: &Config, // Adjust the path as necessary to match your project structure
|
||||||
|
command: &str,
|
||||||
|
channel: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let parts: Vec<&str> = command.split_whitespace().collect();
|
||||||
|
let command_type = parts.get(1).unwrap_or(&"");
|
||||||
|
|
||||||
|
if *command_type == "random" && parts.len() == 2 {
|
||||||
|
handle_random(writer, config, channel).await?;
|
||||||
|
} else if *command_type == "list"{
|
||||||
|
handle_list(writer, config, channel, Some(parts.get(3).unwrap_or(&""))).await?;
|
||||||
|
} else {
|
||||||
|
handle_specific_file(writer, config, channel, &parts).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_random<W: AsyncWriteExt + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
config: &Config,
|
||||||
|
channel: &str,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
if let Some(dir) = config.ascii_art.as_ref() {
|
||||||
|
if let Some(random_file) = select_random_file(dir) {
|
||||||
|
send_ansi_art(writer, &random_file, config.pump_delay, channel).await?;
|
||||||
|
} else {
|
||||||
|
writer.write_all(format!("PRIVMSG {} :No files found\r\n", channel).as_bytes()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list<W: AsyncWriteExt + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
config: &Config,
|
||||||
|
channel: &str,
|
||||||
|
subdirectory: Option<&str>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let base_dir = config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string());
|
||||||
|
let dir = if let Some(subdir) = subdirectory {
|
||||||
|
format!("{}/{}", base_dir, subdir)
|
||||||
|
} else {
|
||||||
|
base_dir
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = fs::read_dir(&dir)
|
||||||
|
.map_err(|_| "Failed to read directory")?
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.map(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
let display_name = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
|
||||||
|
if path.is_dir() {
|
||||||
|
format!("{}/", display_name)
|
||||||
|
} else {
|
||||||
|
display_name.strip_suffix(".txt").unwrap_or(&display_name).to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
writer.write_all(format!("PRIVMSG {} :No files or directories found\r\n", channel).as_bytes()).await?;
|
||||||
|
} else {
|
||||||
|
writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, entries).as_bytes()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_specific_file<W: AsyncWriteExt + Unpin>(
|
||||||
|
writer: &mut W,
|
||||||
|
config: &Config,
|
||||||
|
channel: &str,
|
||||||
|
parts: &[&str],
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let file_name = if parts.len() >= 3 {
|
||||||
|
parts[2..].join(" ")
|
||||||
|
} else {
|
||||||
|
parts[2].to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = format!("{}/{}.txt", config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string()), file_name);
|
||||||
|
|
||||||
|
send_ansi_art(writer, &file_path, config.pump_delay, channel).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,10 +5,11 @@ pub async fn start_sasl_auth<W: tokio::io::AsyncWriteExt + Unpin>(
|
|||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
mechanism: &str,
|
mechanism: &str,
|
||||||
nickname: &str,
|
nickname: &str,
|
||||||
|
realname: &str,
|
||||||
capabilities: Option<Vec<String>>) -> Result<(), Box<dyn std::error::Error>> {
|
capabilities: Option<Vec<String>>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
writer.write_all(b"CAP LS 302\r\n").await?;
|
writer.write_all(b"CAP LS 302\r\n").await?;
|
||||||
|
|
||||||
nickme(writer, nickname).await?;
|
nickme(writer, nickname, realname).await?;
|
||||||
|
|
||||||
if let Some(caps) = capabilities {
|
if let Some(caps) = capabilities {
|
||||||
if !caps.is_empty() {
|
if !caps.is_empty() {
|
||||||
|
Loading…
Reference in New Issue
Block a user