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
/.pre-commit-config.yaml
.env
# Added by cargo
#
# already existing elements were commented out
#/target
buscemi.ron

8
.gitignore vendored
View File

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

180
Cargo.lock generated
View File

@ -17,6 +17,21 @@ 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"
@ -87,9 +102,15 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
name = "buscemi"
version = "0.1.0"
dependencies = [
"anyhow",
"poise",
"rand",
"ron",
"serde",
"tokio",
"tracing",
"tracing-forest",
"tracing-subscriber",
]
[[package]]
@ -123,9 +144,12 @@ 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",
]
@ -416,7 +440,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -595,6 +619,12 @@ 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"
@ -629,6 +659,15 @@ 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"
@ -668,10 +707,20 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"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"
@ -716,6 +765,12 @@ 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"
@ -860,6 +915,15 @@ 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"
@ -923,6 +987,17 @@ 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"
@ -1056,7 +1131,7 @@ dependencies = [
"serde",
"serde-value",
"serde_json",
"time",
"time 0.3.20",
"tokio",
"tracing",
"typemap_rev",
@ -1074,6 +1149,15 @@ 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"
@ -1160,6 +1244,27 @@ 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"
@ -1295,6 +1400,53 @@ 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]]
@ -1396,6 +1548,22 @@ 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"
@ -1412,6 +1580,12 @@ 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,6 +6,12 @@ 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,6 +58,7 @@
rust-analyzer
rustfmt
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 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
// can we eventually serialize this to disk for cheap persistence?
#[derive(Default)]
pub struct Data {
facts: Mutex<HashMap<String, String>>,
/// a brain stores data for a single guild
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Brain {
facts: 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`
pub fn commands() -> Vec<poise::Command<Data, Error>> {
@ -23,14 +94,14 @@ async fn help(
#[description = "optional command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<(), Error> {
) -> 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<(), Error> {
async fn ping(ctx: Context<'_>) -> Result<()> {
let vowel = {
let mut rng = rand::thread_rng();
let vowels = ['a', 'e', 'o', 'u', 'y'];
@ -48,30 +119,30 @@ async fn eight_ball(
#[rename = "question"]
#[description = "a yes or no question"]
_question: String,
) -> Result<(), Error> {
) -> 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.",
"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()
};
@ -101,17 +172,19 @@ async fn know(
#[rest]
#[description = "the definition of the thing"]
is: String,
) -> Result<(), Error> {
) -> 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 = {
let mut facts = ctx.data().facts.lock().unwrap();
facts.insert(that.to_string(), is.to_string())
};
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 {
@ -128,15 +201,17 @@ async fn what(
#[rest]
#[description = "the thing you want to look up"]
is: String,
) -> Result<(), Error> {
let message = {
let facts = ctx.data().facts.lock().unwrap();
) -> Result<()> {
let message = ctx
.data()
.with_guild(ctx.guild_id().unwrap(), |brain| {
let is = is.strip_prefix("is ").unwrap_or(is.as_str());
facts.get(is).map_or_else(
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(())
}

View File

@ -1,23 +1,73 @@
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() {
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 {
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()
};
let framework = poise::Framework::builder()
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 {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Default::default())
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
})
})
});
println!("it's beneath me. i'm mr. pink. let's move on.");
framework.run().await.unwrap();
}