From c935d725aa1349c2bc5b86ddc852f2f4574983bf Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Thu, 11 Apr 2024 17:03:25 -0500 Subject: [PATCH] Basics are in place --- .gitignore | 1 + Cargo.lock | 337 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/chatbot.rs | 185 ++++++++++++++++++++++++++ src/config.rs | 76 +++++++---- src/main.rs | 45 ++++--- src/observe.rs | 4 +- src/prelude.rs | 1 + src/webserver.rs | 20 ++- 9 files changed, 625 insertions(+), 45 deletions(-) create mode 100644 src/chatbot.rs diff --git a/.gitignore b/.gitignore index 228f69f..1d33d09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /.direnv /.pre-commit-config.yaml +/conf.toml diff --git a/Cargo.lock b/Cargo.lock index 740b784..afda21a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,18 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -284,6 +296,7 @@ dependencies = [ "axum", "clap", "color-eyre", + "config", "discord", "redact", "reqwest", @@ -374,6 +387,55 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.14", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -390,6 +452,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -399,6 +470,22 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.8" @@ -444,6 +531,16 @@ dependencies = [ "serde", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "discord" version = "0.9.0" @@ -466,6 +563,15 @@ dependencies = [ "websocket", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "ed25519" version = "1.5.3" @@ -657,6 +763,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check 0.9.4", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -712,6 +828,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" @@ -974,6 +1096,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "language-tags" version = "0.2.2" @@ -1004,6 +1137,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1093,6 +1232,12 @@ dependencies = [ "unicase 2.7.0", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1155,6 +1300,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1269,6 +1424,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -1306,6 +1471,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1318,6 +1489,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -1673,6 +1889,28 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.5.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1820,6 +2058,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1862,6 +2109,17 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2053,6 +2311,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2132,6 +2399,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2250,6 +2551,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicase" version = "1.4.2" @@ -2289,6 +2602,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "url" version = "1.7.2" @@ -2663,6 +2982,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -2672,3 +3000,12 @@ dependencies = [ "cfg-if 1.0.0", "windows-sys 0.48.0", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 6657b64..2531355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.82" axum = "0.7.5" clap = "4.5.4" color-eyre = "0.6.3" +config = "0.14.0" discord = { git = "https://github.com/SpaceManiac/discord-rs" } redact = { version = "0.1.9", features = ["serde"] } reqwest = { version = "0.12.3", features = ["json", "socks"] } diff --git a/src/chatbot.rs b/src/chatbot.rs new file mode 100644 index 0000000..40637a7 --- /dev/null +++ b/src/chatbot.rs @@ -0,0 +1,185 @@ +// TODO: family reminders? +// TODO: connect to Discord +// TODO: handle messages +// TODO: data persistence? (sqlite? sled?) + +use std::{any::Any, future}; + +use discord::{ + model::{Event, Message}, + Discord, +}; + +use crate::prelude::*; + +pub async fn start(conf: Arc) -> Result<()> { + if conf.discord.is_none() { + warn!("Chatbot starting without Discord token. Nothing will happen."); + // wait forever + future::pending::<()>().await; + } + + let discord = Discord::from_bot_token( + conf.discord + .as_ref() + .unwrap() + .bot_token + .clone() + .expose_secret(), + )?; + + let (mut conn, ready_ev) = discord.connect()?; + info!("Discord connection ready: {ready_ev:?}"); + while let Ok(event) = conn.recv_event() { + tokio::spawn(async move { + match handle_event(event).await { + Ok(_) => {} + Err(err) => { + error!("Failed to handle Discord event: {err}") + } + } + }); + } + + Ok(()) +} + +pub async fn handle_event(event: Event) -> Result<()> { + match event { + Event::Ready(_) => return Ok(ignore_event(event)), + Event::Resumed { trace: _ } => return Ok(ignore_event(event)), + Event::UserUpdate(_) => return Ok(ignore_event(event)), + Event::UserNoteUpdate(_, _) => return Ok(ignore_event(event)), + Event::UserSettingsUpdate { + detect_platform_accounts: _, + developer_mode: _, + enable_tts_command: _, + inline_attachment_media: _, + inline_embed_media: _, + locale: _, + message_display_compact: _, + render_embeds: _, + server_positions: _, + show_current_game: _, + status: _, + theme: _, + convert_emoticons: _, + friend_source_flags: _, + } => return Ok(ignore_event(event)), + Event::UserServerSettingsUpdate(_) => return Ok(ignore_event(event)), + Event::VoiceStateUpdate(_, _) => return Ok(ignore_event(event)), + Event::VoiceServerUpdate { + server_id: _, + channel_id: _, + endpoint: _, + token: _, + } => return Ok(ignore_event(event)), + Event::CallCreate(_) => return Ok(ignore_event(event)), + Event::CallUpdate { + channel_id: _, + message_id: _, + region: _, + ringing: _, + } => return Ok(ignore_event(event)), + Event::CallDelete(_) => return Ok(ignore_event(event)), + Event::ChannelRecipientAdd(_, _) => return Ok(ignore_event(event)), + Event::ChannelRecipientRemove(_, _) => return Ok(ignore_event(event)), + Event::TypingStart { + channel_id: _, + user_id: _, + timestamp: _, + } => return Ok(ignore_event(event)), + Event::PresenceUpdate { + presence: _, + server_id: _, + roles: _, + } => return Ok(ignore_event(event)), + Event::PresencesReplace(_) => return Ok(ignore_event(event)), + Event::RelationshipAdd(_) => return Ok(ignore_event(event)), + Event::RelationshipRemove(_, _) => return Ok(ignore_event(event)), + Event::MessageCreate(msg) => return Ok(handle_message(msg)), + Event::MessageUpdate { + id: _, + channel_id: _, + kind: _, + content: _, + nonce: _, + tts: _, + pinned: _, + timestamp: _, + edited_timestamp: _, + author: _, + mention_everyone: _, + mentions: _, + mention_roles: _, + attachments: _, + embeds: _, + } => return Ok(ignore_event(event)), + Event::MessageAck { + channel_id: _, + message_id: _, + } => return Ok(ignore_event(event)), + Event::MessageDelete { + channel_id: _, + message_id: _, + } => return Ok(ignore_event(event)), + Event::MessageDeleteBulk { + channel_id: _, + ids: _, + } => return Ok(ignore_event(event)), + Event::ServerCreate(_) => return Ok(ignore_event(event)), + Event::ServerUpdate(_) => return Ok(ignore_event(event)), + Event::ServerDelete(_) => return Ok(ignore_event(event)), + Event::ServerMemberAdd(_, _) => return Ok(ignore_event(event)), + Event::ServerMemberUpdate { + server_id: _, + roles: _, + user: _, + nick: _, + } => return Ok(ignore_event(event)), + Event::ServerMemberRemove(_, _) => return Ok(ignore_event(event)), + Event::ServerMembersChunk(_, _) => return Ok(ignore_event(event)), + Event::ServerSync { + server_id: _, + large: _, + members: _, + presences: _, + } => return Ok(ignore_event(event)), + Event::ServerRoleCreate(_, _) => return Ok(ignore_event(event)), + Event::ServerRoleUpdate(_, _) => return Ok(ignore_event(event)), + Event::ServerRoleDelete(_, _) => return Ok(ignore_event(event)), + Event::ServerBanAdd(_, _) => return Ok(ignore_event(event)), + Event::ServerBanRemove(_, _) => return Ok(ignore_event(event)), + Event::ServerIntegrationsUpdate(_) => return Ok(ignore_event(event)), + Event::ServerEmojisUpdate(_, _) => return Ok(ignore_event(event)), + Event::ChannelCreate(_) => return Ok(ignore_event(event)), + Event::ChannelUpdate(_) => return Ok(ignore_event(event)), + Event::ChannelDelete(_) => return Ok(ignore_event(event)), + Event::ChannelPinsAck { + channel_id: _, + timestamp: _, + } => return Ok(ignore_event(event)), + Event::ChannelPinsUpdate { + channel_id: _, + last_pin_timestamp: _, + } => return Ok(ignore_event(event)), + Event::ReactionAdd(_) => return Ok(ignore_event(event)), + Event::ReactionRemove(_) => return Ok(ignore_event(event)), + Event::Unknown(_, _) => return Ok(ignore_event(event)), + _ => return Ok(ignore_event(event)), + } +} + +pub fn ignore_event(event: Event) -> () { + let event_type_id = event.type_id(); + info!("Ignoring Discord event of type: {event_type_id:?}") +} + +pub fn handle_message(msg: Message) -> () { + info!( + "Recieved Discord message in channel {} with content: {}", + msg.channel_id, msg.content + ); + + return (); +} diff --git a/src/config.rs b/src/config.rs index 3a019c3..486d04e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,48 +1,76 @@ -use std::fmt::Debug; +use std::{collections::HashMap, fmt::Debug}; + +use config::{builder::DefaultState, Config as CConfig, ConfigBuilder}; +use redact::serde::redact_secret; use crate::prelude::*; +const CURRENT_VERSION: u64 = 1; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Discord { + #[serde(serialize_with = "redact_secret")] + pub bot_token: Secret, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OpenAI { + #[serde(serialize_with = "redact_secret")] + pub token: Secret, +} + // pub static ref CONFIG: Config = Config::load_or_defaults(); +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Config { pub version: u64, - pub secret: String, + pub discord: Option, + pub open_ai: Option, + + #[serde(serialize_with = "redact_secret")] + pub secret: Secret, } impl Config { - /// Loads a configuration via the predetermined methods, overlaying a - /// default configuration. Returns any relevant errors that occurred - /// during loading to be reported later to the user after logging (or other - /// reporting faculties) have been initialized. - pub fn load_or_defaults() -> (Self, Option) { - let config = Self::defaults(); - (config, None) + pub fn load() -> Result { + let c = Self::builder()?.build()?; + Ok(c.try_deserialize()?) } - pub fn defaults() -> Self { - Self { - version: 1, - secret: "this is a secret!".to_owned(), - } + pub fn builder() -> Result> { + // TODO: log whether or not we were able to load conf.toml? + Ok(Self::default_builder() + .map_err(Error::from)? + .add_source(config::File::from(std::path::PathBuf::from("./conf.toml")).required(false)) + .add_source(config::Environment::with_prefix("chatbot").separator("__"))) } -} -impl Debug for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Config") - .field("version", &self.version) - .finish() + pub fn default_builder() -> Result> { + Ok(CConfig::builder() + .set_default("version", CURRENT_VERSION)? + .set_default("discord", Option::>::None)? + .set_default("open_ai", Option::>::None)? + .set_default("secret", "this is a secret!")?) + } + + pub fn defaults() -> Result { + let c = Self::default_builder()?.build()?; + Ok(c.try_deserialize()?) } } #[cfg(test)] mod tests { - use crate::Config; + use crate::prelude::*; #[test] - fn config_does_not_leak_secrets() { - let defaults = Config::defaults(); + fn config_does_not_leak_secrets() -> Result<()> { + let defaults = Config::default_builder()? + .set_override("discord.bot_token", Secret::new("THIS_IS_MY_BOT_TOKEN"))? + .build()?; let s = format!("{defaults:?}"); - assert!(!s.contains("secret")); + println!("{s}"); + assert!(!s.contains("THIS_IS_MY_BOT_TOKEN")); assert!(!s.contains("this is a secret")); + Ok(()) } } diff --git a/src/main.rs b/src/main.rs index df5b97c..4074db9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] +mod chatbot; mod config; mod minecraft_server_status; mod observe; @@ -10,22 +11,34 @@ mod webserver; use crate::prelude::*; #[tokio::main] -async fn main() { - let (conf, conf_err) = Config::load_or_defaults(); - observe::setup_logging(&conf); - +async fn main() -> Result<()> { + let conf: Arc = match Config::load() { + Err(err) => { + error!("Error loading configuration: {err}"); + return Err(err); + } + Ok(conf) => Arc::new(conf), + }; + observe::setup_logging(conf.clone()); debug!("Configuration: {conf:?}"); - if let Some(err) = conf_err { - warn!("Error loading configuration: {err}"); + + let mut set = tokio::task::JoinSet::new(); + + set.spawn(webserver::start(conf.clone())); + set.spawn(chatbot::start(conf.clone())); + + let result = set.join_next().await; + match result { + Some(Err(err)) => { + error!("One of the JoinSet tasks encountered a JoinError: {err}"); + Err(err.into()) + } + Some(Ok(Err(err))) => { + error!("One of the JoinSet tasks encountered an error: {err}"); + Err(err) + } + _ => { + unreachable!("One of the primary spawned tasks finished. Exiting..."); + } } - - // TODO: oh yeah we need an HTTP server to handle minecraft server status stuff - let server = webserver::start(&conf); - - // TODO: family reminders? - // TODO: connect to Discord - // TODO: handle messages - // TODO: data persistence? (sqlite? sled?) - - server.await } diff --git a/src/observe.rs b/src/observe.rs index e91bc77..9576716 100644 --- a/src/observe.rs +++ b/src/observe.rs @@ -3,12 +3,12 @@ use tracing_subscriber::EnvFilter; use crate::prelude::*; -pub fn setup_logging(_conf: &Config) { +pub fn setup_logging(_conf: Arc) { color_eyre::install().expect("Failed to install color_eyre"); let filter = EnvFilter::builder() .with_default_directive(LevelFilter::TRACE.into()) - .parse_lossy("trace,chatbot=trace"); + .parse_lossy("info,chatbot=trace"); tracing_subscriber::fmt().with_env_filter(filter).init(); } diff --git a/src/prelude.rs b/src/prelude.rs index e68b600..b4d223a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -6,4 +6,5 @@ pub use anyhow::Error; pub use anyhow::Result; pub use redact::Secret; pub use serde::{Deserialize, Serialize}; +pub use std::sync::Arc; pub use tracing::{debug, error, info, trace, warn}; diff --git a/src/webserver.rs b/src/webserver.rs index 6d2b673..0f54430 100644 --- a/src/webserver.rs +++ b/src/webserver.rs @@ -10,24 +10,38 @@ use reqwest::StatusCode; use crate::{minecraft_server_status::MinecraftServerStatus, prelude::*}; struct WebserverError(anyhow::Error); -type Result = std::result::Result; +type WebserverResult = std::result::Result; -pub async fn start(_conf: &Config) { +pub async fn start(_conf: Arc) -> Result<()> { let app = Router::new() .route("/", get(hello_world)) + .route("/health", get(health)) .route("/minecraft-server-status", get(minecraft_server_status)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); info!("Listening on {:?}", listener); axum::serve(listener, app).await.unwrap(); + + Ok(()) } async fn hello_world() -> impl IntoResponse { "Hello, World!".to_owned() } -async fn minecraft_server_status() -> Result { +#[derive(Serialize, Debug)] +struct Health { + status: String, +} + +async fn health() -> impl IntoResponse { + Json(Health { + status: "online".to_owned(), + }) +} + +async fn minecraft_server_status() -> WebserverResult { let c = Arc::new(MinecraftServerStatus::try_new()?); let reqs: Vec<&str> = vec!["h.lyte.dev:26965", "ourcraft.lyte.dev"]; let mut futures = Vec::with_capacity(reqs.len());