Compare commits

..

No commits in common. "34fe58d89bf701e4dfe920c63aa5ec90c490269f" and "dccd88f5e91fc2ded5093c58635c761b3e8810d0" have entirely different histories.

8 changed files with 70 additions and 367 deletions

View File

@ -3,4 +3,10 @@
/target
/.pre-commit-config.yaml
.env
buscemi.ron
# Added by cargo
#
# already existing elements were commented out
#/target

8
.gitignore vendored
View File

@ -3,4 +3,10 @@
/target
/.pre-commit-config.yaml
.env
buscemi.ron
# Added by cargo
#
# already existing elements were commented out
#/target

180
Cargo.lock generated
View File

@ -17,21 +17,6 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "async-trait"
version = "0.1.64"
@ -102,15 +87,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
name = "buscemi"
version = "0.1.0"
dependencies = [
"anyhow",
"poise",
"rand",
"ron",
"serde",
"tokio",
"tracing",
"tracing-forest",
"tracing-subscriber",
]
[[package]]
@ -144,12 +123,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
@ -440,7 +416,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
]
[[package]]
@ -619,12 +595,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.139"
@ -659,15 +629,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -707,20 +668,10 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.45.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@ -765,12 +716,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -915,15 +860,6 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
@ -987,17 +923,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "ron"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
dependencies = [
"base64 0.13.1",
"bitflags",
"serde",
]
[[package]]
name = "rustls"
version = "0.20.8"
@ -1131,7 +1056,7 @@ dependencies = [
"serde",
"serde-value",
"serde_json",
"time 0.3.20",
"time",
"tokio",
"tracing",
"typemap_rev",
@ -1149,15 +1074,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@ -1244,27 +1160,6 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.20"
@ -1400,53 +1295,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-forest"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "119324027fc01804d9f83aefb7d80fda2e8fbe7c28e0acc59187cbd751a12915"
dependencies = [
"ansi_term",
"chrono",
"serde",
"smallvec",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -1548,22 +1396,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
@ -1580,12 +1412,6 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@ -6,12 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.69"
poise = "0.5.2"
rand = "0.8.5"
ron = "0.8.0"
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.25.0", features = ["full"] }
tracing = "0.1.37"
tracing-forest = { version = "0.1.5", features = ["full"] }
tracing-subscriber = "0.3.16"

View File

@ -58,7 +58,6 @@
rust-analyzer
rustfmt
clippy
docker-client
;
};
};

View File

@ -1,3 +0,0 @@
# this is a hack to force nixpacks on fly.io to build without musl
[toolchain]
channel = "stable"

View File

@ -1,86 +1,15 @@
use std::collections::HashMap;
use std::path::Path;
use std::{collections::HashMap, sync::Mutex};
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)]
/// this is where we keep data for the bot
// can we eventually serialize this to disk for cheap persistence?
#[derive(Default)]
pub struct Data {
file: Mutex<File>,
brain: Mutex<HashMap<serenity::GuildId, Brain>>,
facts: Mutex<HashMap<String, String>>,
}
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 content = String::new();
file.read_to_string(&mut content).await?;
let brain = if content.is_empty() {
tracing::info!("initializing brain at {}", path.as_ref().display());
Default::default()
} else {
let brain = ron::from_str(content.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>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
/// all of the commands supported by `buscemi`
pub fn commands() -> Vec<poise::Command<Data, Error>> {
@ -94,14 +23,14 @@ async fn help(
#[description = "optional command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<()> {
) -> Result<(), Error> {
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<()> {
async fn ping(ctx: Context<'_>) -> Result<(), Error> {
let vowel = {
let mut rng = rand::thread_rng();
let vowels = ['a', 'e', 'o', 'u', 'y'];
@ -119,30 +48,30 @@ async fn eight_ball(
#[rename = "question"]
#[description = "a yes or no question"]
_question: String,
) -> Result<()> {
) -> Result<(), Error> {
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.",
"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()
};
@ -172,19 +101,17 @@ async fn know(
#[rest]
#[description = "the definition of the thing"]
is: String,
) -> Result<()> {
) -> Result<(), Error> {
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 old = {
let mut facts = ctx.data().facts.lock().unwrap();
facts.insert(that.to_string(), is.to_string())
};
let message = if let Some(old) = old {
format!("ok, {that} is {is} (changed from {old})")
} else {
@ -201,17 +128,15 @@ async fn what(
#[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;
) -> Result<(), Error> {
let message = {
let facts = ctx.data().facts.lock().unwrap();
let is = is.strip_prefix("is ").unwrap_or(is.as_str());
facts.get(is).map_or_else(
|| format!("i don't know what {is} is"),
|definition| format!("{is} is {definition}"),
)
};
ctx.say(message).await?;
Ok(())
}

View File

@ -1,73 +1,23 @@
mod commands;
use anyhow::{Error, Result};
use poise::serenity_prelude as serenity;
use tracing_forest::{util::EnvFilter, ForestLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry};
const DISCORD_TOKEN: &str = "DISCORD_TOKEN";
const BUSCEMI_DATA: &str = "BUSCEMI_DATA";
const BUSCEMI_DATA_PATH: &str = "./buscemi.ron";
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let framework = init_discord();
tracing::info!("it's beneath me. i'm mr. pink. let's move on.");
framework.run().await?;
Ok(())
}
fn init_tracing() {
Registry::default()
.with(EnvFilter::from_default_env())
.with(ForestLayer::default())
.init()
}
// #[tracing::instrument(level = "debug")]
// async fn pre_command(ctx: commands::Context<'_>) {
// // TODO: set up tracing span?
// }
#[tracing::instrument(level = "debug")]
async fn post_command(ctx: commands::Context<'_>) {
ctx.data()
.commit()
.await
.unwrap_or_else(|e| tracing::error!("failed to commit brain to disk: {e}"))
}
#[tracing::instrument(level = "debug")]
async fn on_error(error: poise::FrameworkError<'_, commands::Data, Error>) {
if let Err(e) = poise::builtins::on_error(error).await {
tracing::error!("unhandled error: {e}")
}
}
fn init_discord() -> poise::FrameworkBuilder<commands::Data, Error> {
async fn main() {
let options = poise::FrameworkOptions {
commands: commands::commands(),
// pre_command: |ctx| Box::pin(pre_command(ctx)),
post_command: |ctx| Box::pin(post_command(ctx)),
on_error: |error| Box::pin(on_error(error)),
..Default::default()
};
poise::Framework::builder()
let framework = poise::Framework::builder()
.options(options)
.token(std::env::var(DISCORD_TOKEN).expect("failed to find DISCORD_TOKEN in env"))
.token(std::env::var("DISCORD_TOKEN").expect("failed to find DISCORD_TOKEN in env"))
.intents(serenity::GatewayIntents::non_privileged())
.setup(|ctx, ready, framework| {
.setup(|ctx, _ready, framework| {
Box::pin(async move {
for guild in &ready.guilds {
poise::builtins::register_in_guild(ctx, &framework.options().commands, guild.id)
.await
.unwrap_or_else(|e| tracing::error!("failed to register commands: {e}"))
}
tracing::debug!("registered commands");
let data_path =
std::env::var(BUSCEMI_DATA).unwrap_or_else(|_| String::from(BUSCEMI_DATA_PATH));
commands::Data::from_path(data_path).await
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Default::default())
})
})
});
println!("it's beneath me. i'm mr. pink. let's move on.");
framework.run().await.unwrap();
}