Compare commits

...

13 Commits

Author SHA1 Message Date
8f38c526cc Make the config file path addressible via env var 2026-05-29 16:47:49 -04:00
a28bbb777c Cargo update 2026-05-29 10:23:21 -04:00
bd7aefd9bd Update robbit link 2026-05-27 01:13:44 -04:00
72caa70606 Add bonk module 2026-02-13 14:56:34 -05:00
26d71ee449 Add ability to tell someone to read the f-ing chat 2026-02-13 14:47:44 -05:00
76c4c0b86d Add time to date module 2026-02-13 14:43:19 -05:00
4df1540bc8 Add history module 2026-01-19 13:41:34 -05:00
a0fcb5d4a8 Use human formatted time 2025-12-29 13:08:23 -05:00
36c98bd6df Change regex 2025-05-05 21:05:06 -07:00
c055beb4e8 Add kick (#2)
* Add kick command

* Made kick only work with my nick
2025-05-05 20:52:54 -07:00
921b10d696 Remove the bully message when a user joins (#1) 2025-05-05 20:32:20 -07:00
f0f64492cc Updated TTB to match birth 2024-10-18 09:01:24 -07:00
cdd4c29464 Readd name as an optional parameter to noemo 2024-04-29 19:44:06 -07:00
15 changed files with 755 additions and 543 deletions

977
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "robbit" name = "robbit"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -13,3 +13,4 @@ futures = {version = "*"}
tokio = {version = "*", features = ["full"]} tokio = {version = "*", features = ["full"]}
regex = "1" regex = "1"
rand = "0.8.5" rand = "0.8.5"
humantime = "2.3.0"

View File

@@ -12,4 +12,4 @@ USER robbit
RUN cargo build -r RUN cargo build -r
CMD ./target/release/robbit CMD ["./target/release/robbit"]

View File

@@ -6,14 +6,14 @@ use regex::Regex;
//is this the best way to do this? probably not //is this the best way to do this? probably not
mod modules; mod modules;
use modules::{bully, lenny, join_rude, grass, noemo, ttb, help, repo,rtfm}; use modules::{bully, lenny, join_rude, grass, noemo, ttb, help, repo,rtfm, kick, history, time_to_date, bonk};
type ModuleFunc = fn(regex::Captures, &Message, &VecDeque<Message>)->String; type ModuleFunc = fn(regex::Captures, &Message, &VecDeque<Message>)->String;
const NUM_MODS:usize = 8; const NUM_MODS:usize = 12;
const MODULES: [(&str, ModuleFunc);NUM_MODS] = [(lenny::PATTERN, lenny::mod_message), (bully::PATTERN, bully::mod_message), (grass::PATTERN, grass::touch_grass), (noemo::PATTERN, noemo::no_emo), (ttb::PATTERN, ttb::time_to_baby), (help::PATTERN, help::help), (repo::PATTERN, repo::link), (rtfm::PATTERN, rtfm::rtfm)]; const MODULES: [(&str, ModuleFunc);NUM_MODS] = [(lenny::PATTERN, lenny::mod_message), (bully::PATTERN, bully::mod_message), (grass::PATTERN, grass::touch_grass), (noemo::PATTERN, noemo::no_emo), (ttb::PATTERN, ttb::time_to_baby), (help::PATTERN, help::help), (repo::PATTERN, repo::link), (rtfm::PATTERN, rtfm::rtfm), (kick::PATTERN, kick::mod_message), (history::PATTERN, history::mod_message), (time_to_date::PATTERN, time_to_date::time_to_date), (bonk::PATTERN, bonk::bonk)];
const MODULE_USAGE: [(&str, &str); NUM_MODS] = [(lenny::NAME, lenny::USAGE), (bully::NAME, bully::USAGE), (grass::NAME, grass::USAGE), (noemo::NAME, noemo::USAGE), (ttb::NAME, ttb::USAGE), (help::NAME, help::USAGE), (repo::NAME, repo::USAGE),(rtfm::NAME, rtfm::USAGE)]; const MODULE_USAGE: [(&str, &str); NUM_MODS] = [(lenny::NAME, lenny::USAGE), (bully::NAME, bully::USAGE), (grass::NAME, grass::USAGE), (noemo::NAME, noemo::USAGE), (ttb::NAME, ttb::USAGE), (help::NAME, help::USAGE), (repo::NAME, repo::USAGE),(rtfm::NAME, rtfm::USAGE), (kick::NAME, kick::USAGE), (history::NAME, history::USAGE), (time_to_date::NAME, time_to_date::USAGE), (bonk::NAME, bonk::USAGE)];
pub fn build_modules() -> Result<Vec<(Regex, ModuleFunc)>, regex::Error> { pub fn build_modules() -> Result<Vec<(Regex, ModuleFunc)>, regex::Error> {
let mut regex_array: Vec<(Regex, ModuleFunc)> = Vec::with_capacity(NUM_MODS); let mut regex_array: Vec<(Regex, ModuleFunc)> = Vec::with_capacity(NUM_MODS);
@@ -29,10 +29,17 @@ pub fn handle(modules: &Vec<(Regex, ModuleFunc)>, message: &Message, message_buf
match &message.command { match &message.command {
PRIVMSG(_,msg) => for (regex, function) in modules{ PRIVMSG(_,msg) => for (regex, function) in modules{
if let Some(captures) = regex.captures(msg.as_str()) { if let Some(captures) = regex.captures(msg.as_str()) {
if msg.contains("$kick") {
if message.source_nickname().unwrap_or_default() == "L3gion" {
return Some((message.response_target().unwrap_or("#lug").to_string(), function(captures, message, message_buf))); return Some((message.response_target().unwrap_or("#lug").to_string(), function(captures, message, message_buf)));
} }
}
else {
return Some((message.response_target().unwrap_or("#lug").to_string(), function(captures, message, message_buf)));
}
}
}, },
JOIN(ref channel,_,_) => return join_rude::join_rude(message.source_nickname().unwrap_or("unknown user"), channel.as_str()), JOIN(channel,_,_) => return kick::bad_user(message.source_nickname().unwrap_or("unknown user"), channel.as_str()),
_ => () _ => ()
} }

View File

@@ -6,9 +6,10 @@ use robbit::{build_modules, handle};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Error>{ async fn main() -> Result<(), Error>{
let max_len = 100; let max_len = 1000;
let config = Config::load("config.toml")?; let config_file = std::env::var("ROBBIT_CONFIG_PATH").unwrap_or("/etc/robbit/config.toml".to_string());
let config = Config::load(config_file)?;
let mut client = Client::from_config(config).await?; let mut client = Client::from_config(config).await?;
client.identify()?; client.identify()?;
@@ -23,10 +24,15 @@ async fn main() -> Result<(), Error>{
let response = handle(&module_pair, &message, &message_buf); let response = handle(&module_pair, &message, &message_buf);
if let Some((target,msg))= response { if let Some((target,msg))= response {
if msg.contains("/kick") {
let message: Vec<&str> = msg.split(" ").collect();
sender.send_kick(target, message[1], "")?;
}
else {
print!("{}",message); print!("{}",message);
sender.send_privmsg(target,msg)?; sender.send_privmsg(target,msg)?;
} }
} else {
if message_buf.len() < max_len { if message_buf.len() < max_len {
message_buf.push_front(message); message_buf.push_front(message);
} }
@@ -35,6 +41,7 @@ async fn main() -> Result<(), Error>{
message_buf.push_front(message); message_buf.push_front(message);
} }
} }
}
Ok(()) Ok(())
} }

18
src/modules/bonk.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::collections::VecDeque;
use irc::proto::Message;
pub const USAGE: &str = "Usage: $bonk <nick>\r\nPut the person identified as nick in horny jail";
pub const NAME: &str = "bonk";
pub const PATTERN: &str = "^\\$bonk( (?P<nick>[^\\s]+))?$";
pub fn bonk(captures: regex::Captures, _message: &Message, _message_buf: &VecDeque<Message>) -> String {
if let Some(nick) = captures.get(2) {
format!("bonk! {} go to horny jail!", nick.as_str())
}
else {
format!("bonk! go to horny jail!")
}
}

View File

@@ -9,9 +9,9 @@ pub fn touch_grass(captures: regex::Captures, message: &Message, _: &VecDeque<Me
let grass_toucher = captures.get(1).unwrap().as_str(); let grass_toucher = captures.get(1).unwrap().as_str();
let complete_message = format!("{} thinks you should go outside and touch some grass, {}", let complete_message = format!("{}: {} thinks you should go outside and touch some grass",
message.source_nickname().unwrap_or("unknown_nick").to_string(), grass_toucher,
grass_toucher); message.source_nickname().unwrap_or("unknown_nick").to_string());
complete_message complete_message
} }

39
src/modules/history.rs Normal file
View File

@@ -0,0 +1,39 @@
use irc::proto::Message;
use std::collections::VecDeque;
use irc::proto::Command::*;
pub const PATTERN: &str = "^\\$history (?P<nick>[^\\s]+) (?P<amount>[0-9]+)";
pub const NAME: &str = "history";
pub const USAGE: &str = "Usage: $history <nick> <amount\r\nThis replays the last <amount> of messages from a user";
pub fn mod_message(captures: regex::Captures, _message: &Message, message_buf: &VecDeque<Message>) -> String {
let amount: usize = captures.get(2).unwrap().as_str().parse().unwrap();
let mut messages: Vec<String> = Vec::with_capacity(amount);
let mut total_message: String = format!("No messages found for user: {}", captures.get(1).unwrap().as_str());
for message in message_buf {
if let Some(nick) = message.source_nickname() {
if nick == captures.get(1).unwrap().as_str() && messages.len() < amount {
match &message.command {
PRIVMSG(_,msg) => {
messages.push(msg.clone());
},
_ => ()
};
}
if messages.len() == amount {
break;
}
}
}
if messages.len() > 0 {
messages.reverse();
total_message = messages.join("\r\n");
}
total_message
}
pub fn usage(message: &Message) -> (String, String) {
(message.response_target().unwrap_or("#lug").to_string(), USAGE.to_string())
}

35
src/modules/kick.rs Normal file
View File

@@ -0,0 +1,35 @@
use irc::proto::Message;
use std::collections::VecDeque;
use regex::Regex;
pub const PATTERN: &str = "^\\$kick (?P<nick>[^\\s]+)";
pub const USAGE: &str = "";
pub const NAME: &str = "kick";
pub fn mod_message(captures: regex::Captures, message: &Message, _message_buf: &VecDeque<Message>) -> String {
let to_be_kicked = captures.get(1).unwrap().as_str();
format!("/kick {}", to_be_kicked)
}
pub fn usage(message: &Message) -> (String, String) {
(message.response_target().unwrap_or("#lug").to_string(), USAGE.to_string())
}
pub fn bad_user(user: &str, channel: &str) -> Option<(String, String)> {
let regex = match Regex::new(".*[dD]ick[rR]id(er|ing).*") {
Ok(r) => r,
Err(_) => {
return None;
}
};
if regex.is_match(user) {
Some((channel.to_string(), format!("/kick {}", user)))
}
else {
None
}
}

View File

@@ -7,4 +7,7 @@ pub mod noemo;
pub mod help; pub mod help;
pub mod repo; pub mod repo;
pub mod rtfm; pub mod rtfm;
pub mod kick;
pub mod history;
pub mod time_to_date;
pub mod bonk;

View File

@@ -1,13 +1,19 @@
use irc::proto::Message; use irc::proto::Message;
use std::collections::VecDeque; use std::collections::VecDeque;
pub const PATTERN: &str = "^\\$noemo\\s*$"; pub const PATTERN: &str = "^\\$noemo( (?P<nick>[^\\s]+))?$";
pub const NAME: &str = "noemo"; pub const NAME: &str = "noemo";
pub const USAGE: &str = "Usage: $noemo <nick>\r\nThis tells the user identified by nick to not be emo"; pub const USAGE: &str = "Usage: $noemo <nick>\r\nThis tells the user identified by nick to not be emo, nick is optional";
pub fn no_emo(_: regex::Captures, message: &Message, _: &VecDeque<Message>) -> String { pub fn no_emo(captures : regex::Captures, message: &Message, _: &VecDeque<Message>) -> String {
let complete_message = format!("{} thinks you shouldn't be so emo. Take a deep breath and lighten up", let complete_message;
if let Some(person) = captures.get(2) {
complete_message = format!("{}: Don't be so emo. Take a deep breath and lighten up", person.as_str());
}
else {
complete_message = format!("{} thinks you shouldn't be so emo. Take a deep breath and lighten up",
message.source_nickname().unwrap_or("unknown_nick").to_string()); message.source_nickname().unwrap_or("unknown_nick").to_string());
}
complete_message complete_message
} }

View File

@@ -5,7 +5,7 @@ use std::collections::VecDeque;
pub const PATTERN: &str = "^\\$repo\\s*$"; pub const PATTERN: &str = "^\\$repo\\s*$";
pub const NAME: &str = "repo"; pub const NAME: &str = "repo";
pub const USAGE: &str = "Usage: $repo\r\nThis gives the link to the robbit repo"; pub const USAGE: &str = "Usage: $repo\r\nThis gives the link to the robbit repo";
pub const REPO_LINK: &str = "https://github.com/ColinMcKechney/robbit.git"; pub const REPO_LINK: &str = "https://git.mckechney.us/l3gion/robbit.git";
pub fn link(_: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String { pub fn link(_: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String {
format!("{}\r\nPRs are always welcome!", REPO_LINK) format!("{}\r\nPRs are always welcome!", REPO_LINK)

View File

@@ -1,17 +1,27 @@
use irc::proto::Message; use irc::proto::Message;
use std::collections::VecDeque; use std::collections::VecDeque;
pub const PATTERN: &str = "^\\$rtf([cm])$"; pub const PATTERN: &str = "^\\$rtf([cm])( (?P<nick>[^\\s]+))?$";
pub const NAME: &str = "rtfm"; pub const NAME: &str = "rtfm";
pub const USAGE: &str = "Usage: $rtf[cm]\r\nThis tells you read the f-ing manual or chat, whichever is chosen"; pub const USAGE: &str = "Usage: $rtf[cm] <nick>\r\nThis tells you read the f-ing manual or chat, whichever is chosen";
pub fn rtfm(captures: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String { pub fn rtfm(captures: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String {
let c_or_m = captures.get(1).unwrap().as_str(); let c_or_m = captures.get(1).unwrap().as_str();
if let Some(nick) = captures.get(2) {
if c_or_m == "c" {
format!("{}: Read the f-ing chat", nick.as_str())
}
else {
format!("{}: Read the f-ing manual", nick.as_str())
}
}
else {
if c_or_m == "c" { if c_or_m == "c" {
"Read the f-ing chat".to_string() "Read the f-ing chat".to_string()
} }
else { else {
"Read the f-ing manual".to_string() "Read the f-ing manual".to_string()
} }
}
} }

110
src/modules/time_to_date.rs Normal file
View File

@@ -0,0 +1,110 @@
use irc::proto::Message;
use std::collections::VecDeque;
use std::time::Duration;
use chrono::prelude::*;
use humantime::{self, parse_rfc3339_weak};
pub const PATTERN: &str = "^\\$ttd\\s*(.*)$";
pub const NAME: &str = "ttd";
pub const USAGE: &str = "Usage $ttd <datetime>\r\nThis prints the amount of time until the specified datetime";
pub fn time_to_date(captures: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String {
let time_string = captures.get(1).unwrap().as_str();
//Dates + Times (numeric)
let requested_time = if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%D %R") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%D %I:%M %P") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%D %I:%M %p") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%m/%d/%Y %R") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%m/%d/%Y %I:%M %P") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%m/%d/%Y %I:%M %p") {
date_time
}
//Dates (numeric)
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%D") {
NaiveDateTime::from(date)
}
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%m/%d/%Y") {
NaiveDateTime::from(date)
}
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%F") {
NaiveDateTime::from(date)
}
//Time (24hour)
else if let Ok(time) = NaiveTime::parse_from_str(time_string, "%R") {
Local::now().naive_local().date().and_time(time)
}
//Time (12hour)
else if let Ok(time) = NaiveTime::parse_from_str(time_string, "%I:%M %P") {
Local::now().naive_local().date().and_time(time)
}
else if let Ok(time) = NaiveTime::parse_from_str(time_string, "%I:%M %p") {
Local::now().naive_local().date().and_time(time)
}
//Date + Time (written)
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%v %R") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%v %I:%M %P") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%v %I:%M %p") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %y %R") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %y %I:%M %p") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %y %I:%M %P") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %Y %R") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %Y %I:%M %p") {
date_time
}
else if let Ok(date_time) = NaiveDateTime::parse_from_str(time_string, "%B %d, %Y %I:%M %P") {
date_time
}
//Date (written)
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%v") {
NaiveDateTime::from(date)
}
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%B %d, %y") {
NaiveDateTime::from(date)
}
else if let Ok(date) = NaiveDate::parse_from_str(time_string, "%B %d, %Y") {
NaiveDateTime::from(date)
}
//RFC3339 datetime
else if let Ok(date_time) = parse_rfc3339_weak(time_string) {
let tmp: DateTime<Local> = date_time.into();
tmp.naive_local()
}
else {
return "Invalid date given".to_string();
};
let current_time = Local::now().naive_local();
let difference = requested_time - current_time;
let human_difference = humantime::format_duration(Duration::from_secs(difference.num_seconds().abs() as u64));
if difference.num_seconds() < 0 {
format!("Was {} ago",human_difference.to_string())
}
else {
format!("Is in {}", human_difference.to_string())
}
}

View File

@@ -1,6 +1,8 @@
use irc::proto::Message; use irc::proto::Message;
use std::collections::VecDeque; use std::collections::VecDeque;
use chrono::{prelude::*, TimeDelta}; use std::time::Duration;
use chrono::prelude::*;
use humantime;
pub const PATTERN: &str = "^\\$ttb\\s*$"; pub const PATTERN: &str = "^\\$ttb\\s*$";
pub const NAME: &str = "ttb"; pub const NAME: &str = "ttb";
@@ -10,17 +12,14 @@ pub const USAGE: &str = "Usage $ttb\r\nThis prints the number of days until pnut
pub fn time_to_baby(_: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String { pub fn time_to_baby(_: regex::Captures, _: &Message, _: &VecDeque<Message>) -> String {
let local_time: DateTime<Local> = Local::now(); let local_time: DateTime<Local> = Local::now();
let birth_time: DateTime<Local> = Local.with_ymd_and_hms(2024, 10, 17, 00, 00, 00).unwrap(); let birth_time: DateTime<Local> = Local.with_ymd_and_hms(2024, 10, 8, 00, 00, 00).unwrap();
let difference = birth_time - local_time; let difference = local_time - birth_time;
let completed_message; let completed_message;
if difference > TimeDelta::zero() {
completed_message = format!("{} {} until pnutz's baby is due!", difference.num_days(), if difference.num_days() > 1 { "days"} else {"day"} ); let human_difference = humantime::format_duration(Duration::from_secs(difference.num_seconds() as u64));
} completed_message = format!("He's {} old!", human_difference.to_string());
else {
completed_message = "They're past due!".to_string();
}
completed_message completed_message