219 lines
6.2 KiB
Rust
219 lines
6.2 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
use anyhow::{Error, Result};
|
|
use poise::serenity_prelude as serenity;
|
|
use rand::seq::SliceRandom;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::fs::{File, OpenOptions};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::sync::Mutex;
|
|
|
|
/// a brain stores data for a single guild
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
pub struct Brain {
|
|
facts: HashMap<String, String>,
|
|
}
|
|
|
|
/// top level data
|
|
#[derive(Debug)]
|
|
pub struct Data {
|
|
file: Mutex<File>,
|
|
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 mut file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.open(&path)
|
|
.await?;
|
|
let mut contents = String::new();
|
|
file.read_to_string(&mut contents).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 file = Mutex::new(file);
|
|
Ok(Data { file, 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()),
|
|
)?)?;
|
|
let mut file = self.file.lock().await;
|
|
file.set_len(0).await?;
|
|
file.write_all(&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(())
|
|
}
|