Compare commits

...

2 Commits

Author SHA1 Message Date
mat ess 34fe58d89b Use tracing, split brain by guild, commit to disk 2023-03-03 00:18:10 -05:00
mat ess 5a76bf4bc1 Make message style consistent 2023-03-01 19:07:03 -05:00
8 changed files with 368 additions and 71 deletions

View File

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

8
.gitignore vendored
View File

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

180
Cargo.lock generated
View File

@ -17,6 +17,21 @@ dependencies = [
"libc", "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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.64" version = "0.1.64"
@ -87,9 +102,15 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
name = "buscemi" name = "buscemi"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"poise", "poise",
"rand", "rand",
"ron",
"serde",
"tokio", "tokio",
"tracing",
"tracing-forest",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -123,9 +144,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde", "serde",
"time 0.1.45",
"wasm-bindgen",
"winapi", "winapi",
] ]
@ -416,7 +440,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -595,6 +619,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.139" version = "0.2.139"
@ -629,6 +659,15 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
@ -668,10 +707,20 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.45.0", "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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -716,6 +765,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -860,6 +915,15 @@ dependencies = [
"regex-syntax", "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]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.28" version = "0.6.28"
@ -923,6 +987,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.8" version = "0.20.8"
@ -1056,7 +1131,7 @@ dependencies = [
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"time", "time 0.3.20",
"tokio", "tokio",
"tracing", "tracing",
"typemap_rev", "typemap_rev",
@ -1074,6 +1149,15 @@ dependencies = [
"digest", "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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -1160,6 +1244,27 @@ dependencies = [
"syn", "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]] [[package]]
name = "time" name = "time"
version = "0.3.20" version = "0.3.20"
@ -1295,6 +1400,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@ -1396,6 +1548,22 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 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]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -1412,6 +1580,12 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -6,6 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.69"
poise = "0.5.2" poise = "0.5.2"
rand = "0.8.5" rand = "0.8.5"
ron = "0.8.0"
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.25.0", features = ["full"] } 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,6 +58,7 @@
rust-analyzer rust-analyzer
rustfmt rustfmt
clippy clippy
docker-client
; ;
}; };
}; };

3
rust-toolchain.toml Normal file
View File

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

View File

@ -1,15 +1,86 @@
use std::{collections::HashMap, sync::Mutex}; use std::collections::HashMap;
use std::path::Path;
use anyhow::{Error, Result};
use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use tokio::fs::{File, OpenOptions};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Mutex;
/// this is where we keep data for the bot /// a brain stores data for a single guild
// can we eventually serialize this to disk for cheap persistence? #[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Default)] pub struct Brain {
pub struct Data { facts: HashMap<String, String>,
facts: Mutex<HashMap<String, String>>,
} }
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; /// 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 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>;
/// all of the commands supported by `buscemi` /// all of the commands supported by `buscemi`
pub fn commands() -> Vec<poise::Command<Data, Error>> { pub fn commands() -> Vec<poise::Command<Data, Error>> {
@ -23,14 +94,14 @@ async fn help(
#[description = "optional command to show help about"] #[description = "optional command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"] #[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>, command: Option<String>,
) -> Result<(), Error> { ) -> Result<()> {
poise::builtins::help(ctx, command.as_deref(), Default::default()).await?; poise::builtins::help(ctx, command.as_deref(), Default::default()).await?;
Ok(()) Ok(())
} }
/// check that the bot is working /// check that the bot is working
#[poise::command(prefix_command, slash_command)] #[poise::command(prefix_command, slash_command)]
async fn ping(ctx: Context<'_>) -> Result<(), Error> { async fn ping(ctx: Context<'_>) -> Result<()> {
let vowel = { let vowel = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let vowels = ['a', 'e', 'o', 'u', 'y']; let vowels = ['a', 'e', 'o', 'u', 'y'];
@ -48,30 +119,30 @@ async fn eight_ball(
#[rename = "question"] #[rename = "question"]
#[description = "a yes or no question"] #[description = "a yes or no question"]
_question: String, _question: String,
) -> Result<(), Error> { ) -> Result<()> {
let answer = { let answer = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let answers = [ let answers = [
"It is certain.", "it is certain.",
"It is decidedly so.", "it is decidedly so.",
"Without a doubt.", "without a doubt.",
"Yes definitely.", "yes definitely.",
"You may rely on it.", "you may rely on it.",
"As I see it, yes.", "as i see it, yes.",
"Most likely.", "most likely.",
"Outlook good.", "outlook good.",
"Yes.", "yes.",
"Signs point to yes.", "signs point to yes.",
"Reply hazy, try again.", "reply hazy, try again.",
"Ask again later.", "ask again later.",
"Better not tell you now.", "better not tell you now.",
"Cannot predict now.", "cannot predict now.",
"Concentrate and ask again.", "concentrate and ask again.",
"Don't count on it.", "don't count on it.",
"My reply is no.", "my reply is no.",
"My sources say no.", "my sources say no.",
"Outlook not so good.", "outlook not so good.",
"Very doubtful.", "very doubtful.",
]; ];
answers.choose(&mut rng).copied().unwrap() answers.choose(&mut rng).copied().unwrap()
}; };
@ -101,17 +172,19 @@ async fn know(
#[rest] #[rest]
#[description = "the definition of the thing"] #[description = "the definition of the thing"]
is: String, is: String,
) -> Result<(), Error> { ) -> Result<()> {
let (that, is) = if that.as_str() == "that" { let (that, is) = if that.as_str() == "that" {
is.split_once(' ').unwrap() is.split_once(' ').unwrap()
} else { } else {
(that.as_str(), is.as_str()) (that.as_str(), is.as_str())
}; };
let is = is.strip_prefix("is ").unwrap_or(is); let is = is.strip_prefix("is ").unwrap_or(is);
let old = { let old = ctx
let mut facts = ctx.data().facts.lock().unwrap(); .data()
facts.insert(that.to_string(), is.to_string()) .with_guild(ctx.guild_id().unwrap(), |brain| {
}; brain.facts.insert(that.to_string(), is.to_string())
})
.await;
let message = if let Some(old) = old { let message = if let Some(old) = old {
format!("ok, {that} is {is} (changed from {old})") format!("ok, {that} is {is} (changed from {old})")
} else { } else {
@ -128,15 +201,17 @@ async fn what(
#[rest] #[rest]
#[description = "the thing you want to look up"] #[description = "the thing you want to look up"]
is: String, is: String,
) -> Result<(), Error> { ) -> Result<()> {
let message = { let message = ctx
let facts = ctx.data().facts.lock().unwrap(); .data()
let is = is.strip_prefix("is ").unwrap_or(is.as_str()); .with_guild(ctx.guild_id().unwrap(), |brain| {
facts.get(is).map_or_else( let is = is.strip_prefix("is ").unwrap_or(is.as_str());
|| format!("i don't know what {is} is"), brain.facts.get(is).map_or_else(
|definition| format!("{is} is {definition}"), || format!("i don't know what {is} is"),
) |definition| format!("{is} is {definition}"),
}; )
})
.await;
ctx.say(message).await?; ctx.say(message).await?;
Ok(()) Ok(())
} }

View File

@ -1,23 +1,73 @@
mod commands; mod commands;
use anyhow::{Error, Result};
use poise::serenity_prelude as serenity; 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] #[tokio::main]
async fn 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> {
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands: commands::commands(), 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() ..Default::default()
}; };
let framework = poise::Framework::builder() poise::Framework::builder()
.options(options) .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()) .intents(serenity::GatewayIntents::non_privileged())
.setup(|ctx, _ready, framework| { .setup(|ctx, ready, framework| {
Box::pin(async move { Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?; for guild in &ready.guilds {
Ok(Default::default()) 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
}) })
}); })
println!("it's beneath me. i'm mr. pink. let's move on.");
framework.run().await.unwrap();
} }