Make it work, still messy as hell

This commit is contained in:
lemon-sh 2022-01-26 12:58:49 +01:00
parent 307918c108
commit 4e197fc4c0
9 changed files with 172 additions and 157 deletions

View file

@ -1,2 +1,2 @@
[target.x86_64-unknown-linux-musl] [target.x86_64-unknown-linux-musl]
rustflags=["-Ctarget-feature=-crt-static"] rustflags = ["-Ctarget-feature=-crt-static"]

View file

@ -11,6 +11,7 @@ tokio = { version = "1.15", features = ["rt", "macros", "signal"] }
anyhow = "1.0" anyhow = "1.0"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-log = "0.1"
reqwest = "0.11" reqwest = "0.11"
serde_json = "1.0" serde_json = "1.0"
fancy-regex = "0.7" fancy-regex = "0.7"
@ -21,11 +22,12 @@ serde = "1.0"
arrayvec = "0.7" arrayvec = "0.7"
rand = "0.8" rand = "0.8"
meval = "0.2" meval = "0.2"
async-circe = { git = "https://git.karx.xyz/circe/async-circe" }
lazy_static = "1.4" lazy_static = "1.4"
sedregex = "0.2" sedregex = "0.2"
rusqlite = { version = "0.26", features = ["bundled"] } rusqlite = { version = "0.26", features = ["bundled"] }
warp = "0.3" warp = "0.3"
futures-util = "0.3"
irc = { version = "0.15", default-features = false }
[features] [features]
tls = ["async-circe/tls"] tls = ["irc/tls-rust"]

View file

@ -1,35 +0,0 @@
use serde_json::Value::Null;
use tokio::sync::mpsc::Sender;
pub async fn handle_post(
json: serde_json::Value,
tx: Sender<String>,
) -> Result<impl warp::Reply, warp::Rejection> {
if json["commits"] != Null {
let commits = json["commits"].as_array().unwrap();
let repo = &json["repository"]["full_name"].as_str().unwrap().trim();
if commits.len() != 1 {
tx.send(format!("{} new commits on {}:", commits.len(), repo))
.await
.expect("Failed to send string to main thread");
for commit in commits {
let author = &commit["author"]["name"].as_str().unwrap().trim();
let message = &commit["message"].as_str().unwrap().trim();
tx.send(format!("{} - {}", author, message))
.await
.expect("Failed to send string to main thread");
}
} else {
let author = &json["commits"][0]["author"]["name"]
.as_str()
.unwrap()
.trim();
let message = &json["commits"][0]["message"].as_str().unwrap().trim();
tx.send(format!("New commit on {}: {} - {}", repo, message, author))
.await
.expect("Failed to send string to main thread");
}
}
Ok(warp::reply::with_status("Ok", warp::http::StatusCode::OK))
}

View file

@ -24,7 +24,7 @@ impl<T> From<CapacityError<T>> for LeekCapacityError {
type LeekResult = Result<ArrayString<512>, LeekCapacityError>; type LeekResult = Result<ArrayString<512>, LeekCapacityError>;
pub fn mock(input: &str) -> LeekResult { fn mock(input: &str) -> LeekResult {
let mut builder = ArrayString::<512>::new(); let mut builder = ArrayString::<512>::new();
for ch in input.chars() { for ch in input.chars() {
@ -38,7 +38,7 @@ pub fn mock(input: &str) -> LeekResult {
Ok(builder) Ok(builder)
} }
pub fn leetify(input: &str) -> LeekResult { fn leetify(input: &str) -> LeekResult {
let mut builder = ArrayString::<512>::new(); let mut builder = ArrayString::<512>::new();
for ch in input.chars() { for ch in input.chars() {
@ -58,7 +58,7 @@ pub fn leetify(input: &str) -> LeekResult {
Ok(builder) Ok(builder)
} }
pub fn owoify(input: &str) -> LeekResult { fn owoify(input: &str) -> LeekResult {
let mut builder: ArrayString<512> = ArrayString::from("\x1d")?; let mut builder: ArrayString<512> = ArrayString::from("\x1d")?;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut last_char = '\0'; let mut last_char = '\0';
@ -101,3 +101,35 @@ pub fn owoify(input: &str) -> LeekResult {
builder.try_push_str("~~")?; builder.try_push_str("~~")?;
Ok(builder) Ok(builder)
} }
#[derive(Debug)]
pub enum LeekCommand {
Owo,
Leet,
Mock,
}
pub fn execute_leek(
state: &mut crate::AppState,
cmd: LeekCommand,
target: &str,
nick: &str,
) -> anyhow::Result<()> {
match state.last_msgs.get(nick) {
Some(msg) => {
tracing::debug!("Executing {:?} on {:?}", cmd, msg);
let output = match cmd {
LeekCommand::Owo => super::leek::owoify(msg)?,
LeekCommand::Leet => super::leek::leetify(msg)?,
LeekCommand::Mock => super::leek::mock(msg)?,
};
state.client.send_privmsg(target, &output)?;
}
None => {
state
.client
.send_privmsg(target, "No last messages found.")?;
}
}
Ok(())
}

View file

@ -4,13 +4,6 @@ use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
#[derive(Debug)]
pub enum LeekCommand {
Owo,
Leet,
Mock,
}
pub async fn get_waifu_pic(category: &str) -> anyhow::Result<Option<String>> { pub async fn get_waifu_pic(category: &str) -> anyhow::Result<Option<String>> {
let api_resp = reqwest::get(format!("https://api.waifu.pics/sfw/{}", category)) let api_resp = reqwest::get(format!("https://api.waifu.pics/sfw/{}", category))
.await? .await?
@ -40,29 +33,3 @@ pub fn mathbot(
Ok(ArrayString::from("No expression to evaluate")?) Ok(ArrayString::from("No expression to evaluate")?)
} }
} }
pub async fn execute_leek(
state: &mut crate::AppState,
cmd: LeekCommand,
channel: &str,
nick: &str,
) -> anyhow::Result<()> {
match state.last_msgs.get(nick) {
Some(msg) => {
tracing::debug!("Executing {:?} on {:?}", cmd, msg);
let output = match cmd {
LeekCommand::Owo => super::leek::owoify(msg)?,
LeekCommand::Leet => super::leek::leetify(msg)?,
LeekCommand::Mock => super::leek::mock(msg)?,
};
state.client.privmsg(channel, &output).await?;
}
None => {
state
.client
.privmsg(channel, "No last messages found.")
.await?;
}
}
Ok(())
}

View file

@ -1,4 +1,3 @@
pub mod git;
pub mod leek; pub mod leek;
pub mod misc; pub mod misc;
pub mod sed; pub mod sed;

View file

@ -46,7 +46,7 @@ type SedResult = Result<Option<ArrayString<512>>, SedError>;
pub fn resolve(prev_msg: &str, cmd: &str) -> SedResult { pub fn resolve(prev_msg: &str, cmd: &str) -> SedResult {
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new(r"^s/.*/.*").unwrap(); // yes this regex is valid, don't worry about it static ref RE: Regex = Regex::new(r"^s/.*/.*").unwrap();
} }
if RE.is_match(cmd)? { if RE.is_match(cmd)? {

View file

@ -1,24 +1,29 @@
mod bots;
mod database;
mod web_service;
use crate::database::{DbExecutor, ExecutorConnection};
use arrayvec::ArrayString;
use async_circe::{commands::Command, Client, Config};
use bots::title::Titlebot;
use bots::{misc, misc::LeekCommand, sed};
use rspotify::Credentials;
use serde::Deserialize;
use std::fmt::Write; use std::fmt::Write;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::thread; use std::thread;
use std::{collections::HashMap, env}; use std::{collections::HashMap, env};
use arrayvec::ArrayString;
use futures_util::stream::StreamExt;
use irc::client::prelude::Config;
use irc::client::Client;
use irc::proto::{ChannelExt, Command, Prefix};
use rspotify::Credentials;
use serde::Deserialize;
use tokio::select; use tokio::select;
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
use tracing_log::LogTracer;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use crate::bots::{leek, misc, sed, title};
use crate::database::{DbExecutor, ExecutorConnection};
mod bots;
mod database;
mod web_service;
// this will be displayed when the help command is used // this will be displayed when the help command is used
const HELP: &[&str] = &[ const HELP: &[&str] = &[
concat!("=- \x1d\x02Ü\x02berbot\x0f ", env!("CARGO_PKG_VERSION"), " -="), concat!("=- \x1d\x02Ü\x02berbot\x0f ", env!("CARGO_PKG_VERSION"), " -="),
@ -53,7 +58,7 @@ pub struct AppState {
client: Client, client: Client,
last_msgs: HashMap<String, String>, last_msgs: HashMap<String, String>,
last_eval: HashMap<String, f64>, last_eval: HashMap<String, f64>,
titlebot: Titlebot, titlebot: title::Titlebot,
db: ExecutorConnection, db: ExecutorConnection,
git_channel: String, git_channel: String,
} }
@ -62,6 +67,7 @@ pub struct AppState {
struct ClientConf { struct ClientConf {
channels: Vec<String>, channels: Vec<String>,
host: String, host: String,
tls: bool,
mode: Option<String>, mode: Option<String>,
nickname: Option<String>, nickname: Option<String>,
port: u16, port: u16,
@ -76,6 +82,7 @@ struct ClientConf {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
LogTracer::init()?;
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_env("UBERBOT_LOG")) .with_env_filter(EnvFilter::from_env("UBERBOT_LOG"))
.init(); .init();
@ -103,24 +110,29 @@ async fn main() -> anyhow::Result<()> {
.http_listen .http_listen
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 5000))); .unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 5000)));
let config = Config::runtime_config( let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION"));
client_config.channels, let irc_config = Config {
client_config.host, nickname: client_config.nickname,
client_config.mode, username: Some(client_config.username.clone()),
client_config.nickname, realname: Some(client_config.username),
client_config.port, server: Some(client_config.host),
client_config.username, port: Some(client_config.port),
); use_tls: Some(client_config.tls),
channels: client_config.channels,
let mut client = Client::new(config).await?; umodes: client_config.mode,
client.identify().await?; user_info: Some(uber_ver.into()),
version: Some(uber_ver.into()),
..Config::default()
};
let client = Client::from_config(irc_config).await?;
client.identify()?;
let state = AppState { let state = AppState {
prefix: client_config.prefix, prefix: client_config.prefix,
client, client,
last_msgs: HashMap::new(), last_msgs: HashMap::new(),
last_eval: HashMap::new(), last_eval: HashMap::new(),
titlebot: Titlebot::create(spotify_creds).await?, titlebot: title::Titlebot::create(spotify_creds).await?,
db: db_conn, db: db_conn,
git_channel: client_config.git_channel, git_channel: client_config.git_channel,
}; };
@ -146,31 +158,39 @@ async fn executor(
http_listen: SocketAddr, http_listen: SocketAddr,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let web_db = state.db.clone(); let web_db = state.db.clone();
let git_channel = state.git_channel.clone();
select! { select! {
r = web_service::run(web_db, git_tx, http_listen) => r?, r = web_service::run(web_db, git_tx, http_listen) => r?,
r = message_loop(&mut state) => r?, r = message_loop(&mut state) => r?,
r = git_recv.recv() => { r = git_recv.recv() => {
if let Some(message) = r { if let Some(message) = r {
state.client.privmsg(&git_channel, &message).await?; state.client.send_privmsg(&state.git_channel, &message)?;
} }
} }
_ = terminate_signal() => { _ = terminate_signal() => {
tracing::info!("Sending QUIT message"); tracing::info!("Sending QUIT message");
state.client.quit(Some("überbot shutting down")).await?; state.client.send_quit("überbot shutting down")?;
} }
} }
Ok(()) Ok(())
} }
async fn message_loop(state: &mut AppState) -> anyhow::Result<()> { async fn message_loop(state: &mut AppState) -> anyhow::Result<()> {
while let Some(cmd) = state.client.read().await? { let mut stream = state.client.stream()?;
if let Command::PRIVMSG(nick, channel, message) = cmd { while let Some(message) = stream.next().await.transpose()? {
if let Err(e) = handle_privmsg(state, nick, &channel, message).await { if let Command::PRIVMSG(ref origin, content) = message.command {
state if origin.is_channel_name() {
.client if let Some(author) = message.prefix.as_ref().and_then(|p| match p {
.privmsg(&channel, &format!("Error: {}", e)) Prefix::Nickname(name, _, _) => Some(&name[..]),
.await?; _ => None,
}) {
if let Err(e) = handle_privmsg(state, author, origin, content).await {
state
.client
.send_privmsg(origin, &format!("Error: {}", e))?;
}
} else {
tracing::warn!("Couldn't get the author for a message");
}
} }
} }
} }
@ -187,35 +207,35 @@ fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
async fn handle_privmsg( async fn handle_privmsg(
state: &mut AppState, state: &mut AppState,
nick: String, author: &str,
channel: &str, origin: &str,
message: String, content: String,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if !message.starts_with(state.prefix.as_str()) { if !content.starts_with(state.prefix.as_str()) {
if let Some(titlebot_msg) = state.titlebot.resolve(&message).await? { if let Some(titlebot_msg) = state.titlebot.resolve(&content).await? {
state.client.privmsg(&channel, &titlebot_msg).await?; state.client.send_privmsg(origin, &titlebot_msg)?;
} }
if let Some(prev_msg) = state.last_msgs.get(&nick) { if let Some(prev_msg) = state.last_msgs.get(author) {
if let Some(formatted) = sed::resolve(prev_msg, &message)? { if let Some(formatted) = sed::resolve(prev_msg, &content)? {
let mut result = ArrayString::<512>::new(); let mut result = ArrayString::<512>::new();
write!(result, "<{}> {}", nick, formatted)?; write!(result, "<{}> {}", author, formatted)?;
state.client.privmsg(&channel, &result).await?; state.client.send_privmsg(origin, &result)?;
state.last_msgs.insert(nick, formatted.to_string()); state.last_msgs.insert(author.into(), formatted.to_string());
return Ok(()); return Ok(());
} }
} }
state.last_msgs.insert(nick, message); state.last_msgs.insert(author.into(), content);
return Ok(()); return Ok(());
} }
let (command, remainder) = separate_to_space(&message, state.prefix.len()); let (command, remainder) = separate_to_space(&content, state.prefix.len());
tracing::debug!("Command received ({:?}; {:?})", command, remainder); tracing::debug!("Command received ({:?}; {:?})", command, remainder);
match command { match command {
"help" => { "help" => {
for help_line in HELP { for help_line in HELP {
state.client.privmsg(&channel, help_line).await?; state.client.send_privmsg(origin, help_line)?;
} }
} }
"waifu" => { "waifu" => {
@ -225,76 +245,72 @@ async fn handle_privmsg(
.as_ref() .as_ref()
.map(|v| v.as_str()) .map(|v| v.as_str())
.unwrap_or("Invalid category. Valid categories: https://waifu.pics/docs"); .unwrap_or("Invalid category. Valid categories: https://waifu.pics/docs");
state.client.privmsg(&channel, response).await?; state.client.send_privmsg(origin, response)?;
} }
"mock" => { "mock" => {
misc::execute_leek( leek::execute_leek(
state, state,
LeekCommand::Mock, leek::LeekCommand::Mock,
channel, origin,
remainder.unwrap_or(&nick), remainder.unwrap_or(author),
) )?;
.await?;
} }
"leet" => { "leet" => {
misc::execute_leek( leek::execute_leek(
state, state,
LeekCommand::Leet, leek::LeekCommand::Leet,
channel, origin,
remainder.unwrap_or(&nick), remainder.unwrap_or(author),
) )?;
.await?;
} }
"owo" => { "owo" => {
misc::execute_leek(state, LeekCommand::Owo, channel, remainder.unwrap_or(&nick)) leek::execute_leek(
.await?; state,
leek::LeekCommand::Owo,
origin,
remainder.unwrap_or(author),
)?;
} }
"ev" => { "ev" => {
let result = misc::mathbot(nick, remainder, &mut state.last_eval)?; let result = misc::mathbot(author.into(), remainder, &mut state.last_eval)?;
state.client.privmsg(&channel, &result).await?; state.client.send_privmsg(origin, &result)?;
} }
"grab" => { "grab" => {
if let Some(target) = remainder { if let Some(target) = remainder {
if target == nick { if target == author {
state state
.client .client
.privmsg(&channel, "You can't grab yourself") .send_privmsg(target, "You can't grab yourself")?;
.await?;
return Ok(()); return Ok(());
} }
if let Some(prev_msg) = state.last_msgs.get(target) { if let Some(prev_msg) = state.last_msgs.get(target) {
if state.db.add_quote(prev_msg.clone(), target.into()).await { if state.db.add_quote(prev_msg.clone(), target.into()).await {
state.client.privmsg(&channel, "Quote added").await?; state.client.send_privmsg(target, "Quote added")?;
} else { } else {
state state
.client .client
.privmsg(&channel, "A database error has occurred") .send_privmsg(target, "A database error has occurred")?;
.await?;
} }
} else { } else {
state state
.client .client
.privmsg(&channel, "No previous messages to grab") .send_privmsg(target, "No previous messages to grab")?;
.await?;
} }
} else { } else {
state state.client.send_privmsg(origin, "No nickname to grab")?;
.client
.privmsg(&channel, "No nickname to grab")
.await?;
} }
} }
"quot" => { "quot" => {
if let Some(quote) = state.db.get_quote(remainder.map(|v| v.to_string())).await { if let Some(quote) = state.db.get_quote(remainder.map(|v| v.to_string())).await {
let mut resp = ArrayString::<512>::new(); let mut resp = ArrayString::<512>::new();
write!(resp, "\"{}\" ~{}", quote.0, quote.1)?; write!(resp, "\"{}\" ~{}", quote.0, quote.1)?;
state.client.privmsg(&channel, &resp).await?; state.client.send_privmsg(origin, &resp)?;
} else { } else {
state.client.privmsg(&channel, "No quotes found").await?; state.client.send_privmsg(origin, "No quotes found")?;
} }
} }
_ => { _ => {
state.client.privmsg(&channel, "Unknown command").await?; state.client.send_privmsg(origin, "Unknown command")?;
} }
} }
Ok(()) Ok(())

View file

@ -1,4 +1,5 @@
use crate::ExecutorConnection; use crate::ExecutorConnection;
use serde_json::Value::Null;
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use warp::Filter; use warp::Filter;
@ -16,7 +17,7 @@ pub async fn run(
.and(warp::post()) .and(warp::post())
.and(warp::body::json()) .and(warp::body::json())
.and(tx_filter) .and(tx_filter)
.and_then(crate::bots::git::handle_post); .and_then(handle_webhook);
let filter = db_filter.or(tx_filter); let filter = db_filter.or(tx_filter);
warp::serve(filter).run(listen).await; warp::serve(filter).run(listen).await;
@ -36,3 +37,36 @@ async fn handle(db: ExecutorConnection) -> Result<impl warp::Reply, warp::Reject
)) ))
} }
} }
pub async fn handle_webhook(
json: serde_json::Value,
tx: Sender<String>,
) -> Result<impl warp::Reply, warp::Rejection> {
if json["commits"] != Null {
let commits = json["commits"].as_array().unwrap();
let repo = &json["repository"]["full_name"].as_str().unwrap().trim();
if commits.len() != 1 {
tx.send(format!("{} new commits on {}:", commits.len(), repo))
.await
.expect("Failed to send string to main thread");
for commit in commits {
let author = &commit["author"]["name"].as_str().unwrap().trim();
let message = &commit["message"].as_str().unwrap().trim();
tx.send(format!("{} - {}", author, message))
.await
.expect("Failed to send string to main thread");
}
} else {
let author = &json["commits"][0]["author"]["name"]
.as_str()
.unwrap()
.trim();
let message = &json["commits"][0]["message"].as_str().unwrap().trim();
tx.send(format!("New commit on {}: {} - {}", repo, message, author))
.await
.expect("Failed to send string to main thread");
}
}
Ok(warp::reply::with_status("Ok", warp::http::StatusCode::OK))
}