buscemi/src/commands.rs

212 lines
6.0 KiB
Rust

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Error, Result};
use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
/// a brain stores data for a single guild
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Brain {
facts: HashMap<String, String>,
}
/// top level data
#[derive(Debug)]
pub struct Data {
path: PathBuf,
brain: Mutex<HashMap<serenity::GuildId, Brain>>,
}
impl Data {
#[tracing::instrument(fields(path = %path.as_ref().display()))]
pub async fn from_path(path: impl AsRef<Path>) -> Result<Data> {
let contents = tokio::fs::read_to_string(&path).await?;
tracing::debug!("loaded brain contents: {contents}");
let brain = if contents.is_empty() {
tracing::info!("initializing brain at {}", path.as_ref().display());
Default::default()
} else {
let brain = ron::from_str(contents.as_str())?;
tracing::info!("loaded brain from {}", path.as_ref().display());
Mutex::new(brain)
};
let path = path.as_ref().to_path_buf();
Ok(Data { path, brain })
}
#[tracing::instrument]
pub async fn commit(&self) -> Result<()> {
let mut contents = vec![];
self.brain
.lock()
.await
.serialize(&mut ron::Serializer::new(
&mut contents,
Some(Default::default()),
)?)?;
tracing::debug!(
"committing brain contents: {}",
String::from_utf8_lossy(&contents)
);
tokio::fs::write(&self.path, &contents).await?;
tracing::info!("committed brain to disk");
Ok(())
}
#[tracing::instrument(skip(f))]
async fn with_guild<T>(
&self,
guild_id: serenity::GuildId,
mut f: impl FnMut(&mut Brain) -> T,
) -> T {
let mut brain = self.brain.lock().await;
if let Some(guild) = brain.get_mut(&guild_id) {
f(guild)
} else {
let mut guild = Default::default();
let result = f(&mut guild);
brain.insert(guild_id, guild);
result
}
}
}
pub type Context<'a> = poise::Context<'a, Data, Error>;
/// all of the commands supported by `buscemi`
pub fn commands() -> Vec<poise::Command<Data, Error>> {
vec![help(), ping(), eight_ball(), know(), what()]
}
/// print helpful information
#[poise::command(prefix_command, slash_command)]
async fn help(
ctx: Context<'_>,
#[description = "optional command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<()> {
poise::builtins::help(ctx, command.as_deref(), Default::default()).await?;
Ok(())
}
/// check that the bot is working
#[poise::command(prefix_command, slash_command)]
async fn ping(ctx: Context<'_>) -> Result<()> {
let vowel = {
let mut rng = rand::thread_rng();
let vowels = ['a', 'e', 'o', 'u', 'y'];
vowels.choose(&mut rng).copied().unwrap()
};
ctx.say(format!("p{vowel}ng")).await?;
Ok(())
}
/// leave your important decisions up to chance
#[poise::command(prefix_command, slash_command, rename = "8ball")]
async fn eight_ball(
ctx: Context<'_>,
#[rest]
#[rename = "question"]
#[description = "a yes or no question"]
_question: String,
) -> Result<()> {
let answer = {
let mut rng = rand::thread_rng();
let answers = [
"it is certain.",
"it is decidedly so.",
"without a doubt.",
"yes definitely.",
"you may rely on it.",
"as i see it, yes.",
"most likely.",
"outlook good.",
"yes.",
"signs point to yes.",
"reply hazy, try again.",
"ask again later.",
"better not tell you now.",
"cannot predict now.",
"concentrate and ask again.",
"don't count on it.",
"my reply is no.",
"my sources say no.",
"outlook not so good.",
"very doubtful.",
];
answers.choose(&mut rng).copied().unwrap()
};
ctx.say(answer).await?;
Ok(())
}
/// commit some important knowledge to memory
///
/// usage:
/// ```
/// @buscemi (remember|know) [that] <OBJECT> [is] <DEFINITION>
/// ```
/// examples:
/// ```
/// @buscemi remember that x is y
/// @buscemi know foo is bar
/// ```
/// technically works but confusing:
/// ```
/// @buscemi remember 1 2
/// ```
#[poise::command(prefix_command, slash_command, aliases("remember"))]
async fn know(
ctx: Context<'_>,
#[description = "the thing you want to define"] that: String,
#[rest]
#[description = "the definition of the thing"]
is: String,
) -> Result<()> {
let (that, is) = if that.as_str() == "that" {
is.split_once(' ').unwrap()
} else {
(that.as_str(), is.as_str())
};
let is = is.strip_prefix("is ").unwrap_or(is);
let old = ctx
.data()
.with_guild(ctx.guild_id().unwrap(), |brain| {
brain.facts.insert(that.to_string(), is.to_string())
})
.await;
let message = if let Some(old) = old {
format!("ok, {that} is {is} (changed from {old})")
} else {
format!("ok, {that} is {is}")
};
ctx.say(message).await?;
Ok(())
}
/// recall some important knowledge
#[poise::command(prefix_command, slash_command)]
async fn what(
ctx: Context<'_>,
#[rest]
#[description = "the thing you want to look up"]
is: String,
) -> Result<()> {
let message = ctx
.data()
.with_guild(ctx.guild_id().unwrap(), |brain| {
let is = is.strip_prefix("is ").unwrap_or(is.as_str());
brain.facts.get(is).map_or_else(
|| format!("i don't know what {is} is"),
|definition| format!("{is} is {definition}"),
)
})
.await;
ctx.say(message).await?;
Ok(())
}