uberbot/src/main.rs

318 lines
10 KiB
Rust
Raw Normal View History

2022-01-02 14:58:54 -06:00
use std::fmt::Write;
use std::fs::File;
use std::io::Read;
2022-01-13 10:52:39 -06:00
use std::net::SocketAddr;
2022-01-02 14:58:54 -06:00
use std::thread;
use std::{collections::HashMap, env};
2022-01-26 05:58:49 -06:00
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;
2021-12-27 06:39:01 -06:00
use tokio::select;
use tokio::sync::mpsc::{channel, Receiver, Sender};
2022-01-26 05:58:49 -06:00
use tracing_log::LogTracer;
2021-12-27 06:39:01 -06:00
use tracing_subscriber::EnvFilter;
2022-01-26 05:58:49 -06:00
use crate::bots::{leek, misc, sed, title};
use crate::database::{DbExecutor, ExecutorConnection};
mod bots;
mod database;
mod web_service;
2021-12-31 17:51:54 -06:00
// this will be displayed when the help command is used
const HELP: &[&str] = &[
concat!("=- \x1d\x02Ü\x02berbot\x0f ", env!("CARGO_PKG_VERSION"), " -="),
" * waifu <category>",
" * owo/mock/leet [user]",
" * ev <math expression>",
" - This bot also provides titles of URLs and details for Spotify URIs/links. It can also resolve sed expressions."
2021-12-31 17:51:54 -06:00
];
2021-12-27 06:39:01 -06:00
#[cfg(unix)]
async fn terminate_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).unwrap();
let mut sigint = signal(SignalKind::interrupt()).unwrap();
2021-12-31 17:51:54 -06:00
tracing::debug!("Installed ctrl+c handler");
2021-12-27 06:39:01 -06:00
select! {
_ = sigterm.recv() => return,
_ = sigint.recv() => return
2021-12-27 06:39:01 -06:00
}
}
#[cfg(windows)]
async fn terminate_signal() {
use tokio::signal::windows::ctrl_c;
let mut ctrlc = ctrl_c().unwrap();
2021-12-31 17:51:54 -06:00
tracing::debug!("Installed ctrl+c handler");
2021-12-27 06:39:01 -06:00
let _ = ctrlc.recv().await;
}
2022-01-02 14:58:54 -06:00
pub struct AppState {
2021-12-27 14:37:50 -06:00
prefix: String,
client: Client,
last_msgs: HashMap<String, String>,
2021-12-31 17:51:54 -06:00
last_eval: HashMap<String, f64>,
2022-01-26 05:58:49 -06:00
titlebot: title::Titlebot,
2022-01-02 14:58:54 -06:00
db: ExecutorConnection,
2022-01-13 10:52:39 -06:00
git_channel: String,
}
#[derive(Deserialize)]
2021-12-28 06:38:50 -06:00
struct ClientConf {
channels: Vec<String>,
host: String,
2022-01-26 05:58:49 -06:00
tls: bool,
2021-12-28 06:38:50 -06:00
mode: Option<String>,
nickname: Option<String>,
port: u16,
username: String,
spotify_client_id: String,
spotify_client_secret: String,
prefix: String,
2022-01-02 14:58:54 -06:00
db_path: Option<String>,
http_listen: Option<SocketAddr>,
2022-01-13 10:52:39 -06:00
git_channel: String,
2021-12-27 14:37:50 -06:00
}
2021-12-27 06:39:01 -06:00
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
2022-01-26 05:58:49 -06:00
LogTracer::init()?;
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_env("UBERBOT_LOG"))
.init();
2021-12-27 06:39:01 -06:00
2022-01-02 14:58:54 -06:00
let mut file =
File::open(env::var("UBERBOT_CONFIG").unwrap_or_else(|_| "uberbot.toml".to_string()))?;
2021-12-28 06:38:50 -06:00
let mut client_conf = String::new();
file.read_to_string(&mut client_conf)?;
let client_config: ClientConf = toml::from_str(&client_conf)?;
2022-01-02 14:58:54 -06:00
let (db_exec, db_conn) =
DbExecutor::create(client_config.db_path.as_deref().unwrap_or("uberbot.db3"))?;
2022-01-01 16:03:15 -06:00
let exec_thread = thread::spawn(move || {
2022-01-01 15:35:27 -06:00
db_exec.run();
tracing::info!("Database executor has been shut down");
});
let spotify_creds = Credentials::new(
&client_config.spotify_client_id,
&client_config.spotify_client_secret,
);
2022-01-13 10:52:39 -06:00
let http_listen = client_config
.http_listen
2022-01-13 10:52:39 -06:00
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 5000)));
2022-01-26 05:58:49 -06:00
let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION"));
let irc_config = Config {
nickname: client_config.nickname,
username: Some(client_config.username.clone()),
realname: Some(client_config.username),
server: Some(client_config.host),
port: Some(client_config.port),
use_tls: Some(client_config.tls),
channels: client_config.channels,
umodes: client_config.mode,
user_info: Some(uber_ver.into()),
version: Some(uber_ver.into()),
..Config::default()
};
let client = Client::from_config(irc_config).await?;
client.identify()?;
2021-12-27 06:39:01 -06:00
2021-12-27 14:37:50 -06:00
let state = AppState {
prefix: client_config.prefix,
client,
2021-12-27 14:37:50 -06:00
last_msgs: HashMap::new(),
2021-12-31 17:51:54 -06:00
last_eval: HashMap::new(),
2022-01-26 05:58:49 -06:00
titlebot: title::Titlebot::create(spotify_creds).await?,
2022-01-02 14:58:54 -06:00
db: db_conn,
2022-01-13 10:52:39 -06:00
git_channel: client_config.git_channel,
2021-12-27 14:37:50 -06:00
};
let (git_tx, git_recv) = channel(512);
if let Err(e) = executor(state, git_tx, git_recv, http_listen).await {
2021-12-28 07:18:39 -06:00
tracing::error!("Error in message loop: {}", e);
}
2022-01-02 14:58:54 -06:00
if let Err(e) = exec_thread.join() {
tracing::error!("Error while shutting down the database: {:?}", e);
}
2021-12-28 07:18:39 -06:00
tracing::info!("Shutting down");
Ok(())
}
2022-01-15 05:24:27 -06:00
async fn executor(
mut state: AppState,
git_tx: Sender<String>,
2022-01-15 05:24:27 -06:00
mut git_recv: Receiver<String>,
http_listen: SocketAddr,
) -> anyhow::Result<()> {
2022-01-05 17:23:00 -06:00
let web_db = state.db.clone();
2021-12-31 17:51:54 -06:00
select! {
r = web_service::run(web_db, git_tx, http_listen) => r?,
2021-12-31 17:51:54 -06:00
r = message_loop(&mut state) => r?,
2022-01-15 11:18:26 -06:00
r = git_recv.recv() => {
if let Some(message) = r {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(&state.git_channel, &message)?;
2022-01-15 11:18:26 -06:00
}
}
2021-12-31 17:51:54 -06:00
_ = terminate_signal() => {
tracing::info!("Sending QUIT message");
2022-01-26 05:58:49 -06:00
state.client.send_quit("überbot shutting down")?;
2021-12-27 06:39:01 -06:00
}
}
Ok(())
}
2021-12-27 06:39:01 -06:00
2021-12-31 17:51:54 -06:00
async fn message_loop(state: &mut AppState) -> anyhow::Result<()> {
2022-01-26 05:58:49 -06:00
let mut stream = state.client.stream()?;
while let Some(message) = stream.next().await.transpose()? {
if let Command::PRIVMSG(ref origin, content) = message.command {
if origin.is_channel_name() {
if let Some(author) = message.prefix.as_ref().and_then(|p| match p {
Prefix::Nickname(name, _, _) => Some(&name[..]),
_ => 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");
}
}
}
}
2021-12-27 06:39:01 -06:00
Ok(())
}
2022-01-01 15:35:27 -06:00
fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
if let Some(o) = str.find(' ') {
(&str[prefix_len..o], Some(&str[o + 1..]))
} else {
(&str[prefix_len..], None)
}
}
async fn handle_privmsg(
2021-12-27 14:37:50 -06:00
state: &mut AppState,
2022-01-26 05:58:49 -06:00
author: &str,
origin: &str,
content: String,
) -> anyhow::Result<()> {
2022-01-26 05:58:49 -06:00
if !content.starts_with(state.prefix.as_str()) {
if let Some(titlebot_msg) = state.titlebot.resolve(&content).await? {
state.client.send_privmsg(origin, &titlebot_msg)?;
2021-12-27 14:37:50 -06:00
}
2022-01-01 00:52:43 -06:00
2022-01-26 05:58:49 -06:00
if let Some(prev_msg) = state.last_msgs.get(author) {
if let Some(formatted) = sed::resolve(prev_msg, &content)? {
let mut result = ArrayString::<512>::new();
2022-01-26 05:58:49 -06:00
write!(result, "<{}> {}", author, formatted)?;
state.client.send_privmsg(origin, &result)?;
state.last_msgs.insert(author.into(), formatted.to_string());
return Ok(());
2022-01-01 00:52:43 -06:00
}
}
2022-01-26 05:58:49 -06:00
state.last_msgs.insert(author.into(), content);
return Ok(());
}
2022-01-26 05:58:49 -06:00
let (command, remainder) = separate_to_space(&content, state.prefix.len());
tracing::debug!("Command received ({:?}; {:?})", command, remainder);
match command {
2021-12-27 14:37:50 -06:00
"help" => {
2021-12-31 17:51:54 -06:00
for help_line in HELP {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, help_line)?;
2021-12-31 17:51:54 -06:00
}
2021-12-27 14:37:50 -06:00
}
"waifu" => {
let category = remainder.unwrap_or("waifu");
2021-12-31 17:51:54 -06:00
let url = misc::get_waifu_pic(category).await?;
let response = url
.as_ref()
.map(|v| v.as_str())
.unwrap_or("Invalid category. Valid categories: https://waifu.pics/docs");
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, response)?;
}
2021-12-29 14:25:22 -06:00
"mock" => {
2022-01-26 05:58:49 -06:00
leek::execute_leek(
2022-01-02 14:58:54 -06:00
state,
2022-01-26 05:58:49 -06:00
leek::LeekCommand::Mock,
origin,
remainder.unwrap_or(author),
)?;
2021-12-29 14:25:22 -06:00
}
2021-12-29 14:08:49 -06:00
"leet" => {
2022-01-26 05:58:49 -06:00
leek::execute_leek(
2022-01-02 14:58:54 -06:00
state,
2022-01-26 05:58:49 -06:00
leek::LeekCommand::Leet,
origin,
remainder.unwrap_or(author),
)?;
2021-12-30 17:02:12 -06:00
}
"owo" => {
2022-01-26 05:58:49 -06:00
leek::execute_leek(
state,
leek::LeekCommand::Owo,
origin,
remainder.unwrap_or(author),
)?;
2021-12-29 14:08:49 -06:00
}
2021-12-31 17:51:54 -06:00
"ev" => {
2022-01-26 05:58:49 -06:00
let result = misc::mathbot(author.into(), remainder, &mut state.last_eval)?;
state.client.send_privmsg(origin, &result)?;
2021-12-31 17:51:54 -06:00
}
2022-01-01 15:35:27 -06:00
"grab" => {
if let Some(target) = remainder {
2022-01-26 05:58:49 -06:00
if target == author {
2022-01-02 14:58:54 -06:00
state
.client
2022-01-26 05:58:49 -06:00
.send_privmsg(target, "You can't grab yourself")?;
2022-01-02 14:58:54 -06:00
return Ok(());
2022-01-01 15:35:27 -06:00
}
if let Some(prev_msg) = state.last_msgs.get(target) {
if state.db.add_quote(prev_msg.clone(), target.into()).await {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(target, "Quote added")?;
2022-01-01 15:35:27 -06:00
} else {
2022-01-02 14:58:54 -06:00
state
.client
2022-01-26 05:58:49 -06:00
.send_privmsg(target, "A database error has occurred")?;
2022-01-01 15:35:27 -06:00
}
} else {
2022-01-02 14:58:54 -06:00
state
.client
2022-01-26 05:58:49 -06:00
.send_privmsg(target, "No previous messages to grab")?;
2022-01-01 15:35:27 -06:00
}
} else {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, "No nickname to grab")?;
2022-01-01 15:35:27 -06:00
}
}
"quot" => {
if let Some(quote) = state.db.get_quote(remainder.map(|v| v.to_string())).await {
let mut resp = ArrayString::<512>::new();
write!(resp, "\"{}\" ~{}", quote.0, quote.1)?;
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, &resp)?;
2022-01-01 15:35:27 -06:00
} else {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, "No quotes found")?;
2022-01-01 15:35:27 -06:00
}
}
_ => {
2022-01-26 05:58:49 -06:00
state.client.send_privmsg(origin, "Unknown command")?;
}
2021-12-27 06:39:01 -06:00
}
Ok(())
}