diff --git a/.env.example b/.env.example deleted file mode 100644 index 9b080a1..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DISCORD_TOKEN= -SAY_LOGS_CHANNEL= -RUST_LOG=refraction=info,warn,error diff --git a/.envrc b/.envrc index 0475d40..1d67506 100644 --- a/.envrc +++ b/.envrc @@ -1,10 +1,5 @@ -# only use flake when `nix` is present -if command -v nix &> /dev/null; then - if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" - fi - - use flake +if has nix_direnv_version; then + use flake fi dotenv_if_exists diff --git a/src/api/github.rs b/src/api/github.rs new file mode 100644 index 0000000..18fce69 --- /dev/null +++ b/src/api/github.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use eyre::{Context, OptionExt, Result}; +use log::debug; +use octocrab::Octocrab; +use once_cell::sync::Lazy; + +static OCTOCRAB: Lazy> = Lazy::new(octocrab::instance); + +pub async fn get_latest_prism_version() -> Result { + debug!("Fetching the latest version of Prism Launcher"); + + let version = OCTOCRAB + .repos("PrismLauncher", "PrismLauncher") + .releases() + .get_latest() + .await? + .tag_name; + + Ok(version) +} + +pub async fn get_prism_stargazers_count() -> Result { + debug!("Fetching Prism Launcher's stargazer count"); + + let stargazers_count = OCTOCRAB + .repos("PrismLauncher", "PrismLauncher") + .get() + .await + .wrap_err("Couldn't fetch PrismLauncher/PrismLauncher!")? + .stargazers_count + .ok_or_eyre("Couldn't retrieve stargazers_coutn from GitHub!")?; + + Ok(stargazers_count) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index a32d14b..649be3e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,7 @@ use once_cell::sync::Lazy; pub mod dadjoke; +pub mod github; pub mod paste_gg; pub mod pluralkit; pub mod prism_meta; diff --git a/src/commands/general/say.rs b/src/commands/general/say.rs index afeee0d..ebb754c 100644 --- a/src/commands/general/say.rs +++ b/src/commands/general/say.rs @@ -1,6 +1,7 @@ use crate::Context; use eyre::{OptionExt, Result}; +use log::trace; use poise::serenity_prelude::{CreateEmbed, CreateEmbedAuthor, CreateMessage}; /// Say something through the bot @@ -34,7 +35,14 @@ pub async fn say( ctx.say("I said what you said!").await?; } - if let Some(channel_id) = ctx.data().config.discord.channels().say_log_channel_id() { + if let Some(channel_id) = ctx + .data() + .config + .clone() + .discord_config() + .channels() + .say_log_channel_id() + { let log_channel = guild .channels .iter() @@ -54,6 +62,8 @@ pub async fn say( let message = CreateMessage::new().embed(embed); log_channel.1.send_message(ctx, message).await?; + } else { + trace!("Not sending /say log as no channel is set"); } Ok(()) diff --git a/src/commands/general/stars.rs b/src/commands/general/stars.rs index 7cbbd9b..710fa5d 100644 --- a/src/commands/general/stars.rs +++ b/src/commands/general/stars.rs @@ -1,6 +1,6 @@ -use crate::{consts, Context}; +use crate::{api, consts, Context}; -use eyre::{Context as _, Result}; +use eyre::Result; use log::trace; use poise::serenity_prelude::CreateEmbed; use poise::CreateReply; @@ -12,18 +12,17 @@ pub async fn stars(ctx: Context<'_>) -> Result<()> { ctx.defer().await?; - let prismlauncher = ctx - .data() - .octocrab - .repos("PrismLauncher", "PrismLauncher") - .get() - .await - .wrap_err("Couldn't get PrismLauncher/PrismLauncher from GitHub!")?; - - let count = if let Some(count) = prismlauncher.stargazers_count { - count.to_string() + let count = if let Some(storage) = &ctx.data().storage { + if let Ok(count) = storage.get_launcher_stargazer_count().await { + count + } else { + let count = api::github::get_prism_stargazers_count().await?; + storage.cache_launcher_stargazer_count(count).await?; + count + } } else { - "undefined".to_string() + trace!("Not caching launcher stargazer count, as we're running without a storage backend"); + api::github::get_prism_stargazers_count().await? }; let embed = CreateEmbed::new() diff --git a/src/config/bot.rs b/src/config/bot.rs new file mode 100644 index 0000000..7eee8eb --- /dev/null +++ b/src/config/bot.rs @@ -0,0 +1,28 @@ +use log::{info, warn}; + +#[derive(Clone, Debug, Default)] +pub struct Config { + redis_url: Option, +} + +impl Config { + pub fn new(redis_url: Option) -> Self { + Self { redis_url } + } + + pub fn new_from_env() -> Self { + let redis_url = std::env::var("BOT_REDIS_URL").ok(); + + if let Some(url) = &redis_url { + info!("Redis URL is {url}"); + } else { + warn!("BOT_REDIS_URL is empty; features requiring storage will be disabled."); + } + + Self::new(redis_url) + } + + pub fn redis_url(self) -> Option { + self.redis_url + } +} diff --git a/src/config/discord.rs b/src/config/discord.rs index f5f9879..e1051ee 100644 --- a/src/config/discord.rs +++ b/src/config/discord.rs @@ -8,7 +8,7 @@ pub struct RefractionChannels { say_log_channel_id: Option, } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Config { channels: RefractionChannels, } diff --git a/src/config/mod.rs b/src/config/mod.rs index 11bcf09..416472b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,27 +1,32 @@ +mod bot; mod discord; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Config { - pub discord: discord::Config, - pub redis_url: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - discord: discord::Config::default(), - redis_url: "redis://localhost:6379".to_string(), - } - } + bot: bot::Config, + discord: discord::Config, } impl Config { - pub fn new_from_env() -> Self { - let discord = discord::Config::new_from_env(); - + pub fn new(bot_config: bot::Config, discord_config: discord::Config) -> Self { Self { - discord, - ..Default::default() + bot: bot_config, + discord: discord_config, } } + + pub fn new_from_env() -> Self { + let bot = bot::Config::new_from_env(); + let discord = discord::Config::new_from_env(); + + Self::new(bot, discord) + } + + pub fn bot_config(self) -> bot::Config { + self.bot + } + + pub fn discord_config(self) -> discord::Config { + self.discord + } } diff --git a/src/handlers/event/analyze_logs/issues.rs b/src/handlers/event/analyze_logs/issues.rs index d838e88..50822c1 100644 --- a/src/handlers/event/analyze_logs/issues.rs +++ b/src/handlers/event/analyze_logs/issues.rs @@ -1,4 +1,4 @@ -use crate::Data; +use crate::{api, Data}; use eyre::Result; use log::trace; @@ -189,20 +189,15 @@ async fn outdated_launcher(log: &str, data: &Data) -> Result { let version_from_log = captures[0].replace("Prism Launcher version: ", ""); - let storage = &data.storage; - let latest_version = if let Ok(version) = storage.get_launcher_version().await { - version + let latest_version = if let Some(storage) = &data.storage { + if let Ok(version) = storage.get_launcher_version().await { + version + } else { + api::github::get_latest_prism_version().await? + } } else { - let version = data - .octocrab - .repos("PrismLauncher", "PrismLauncher") - .releases() - .get_latest() - .await? - .tag_name; - - storage.cache_launcher_version(&version).await?; - version + trace!("Not caching launcher version, as we're running without a storage backend"); + api::github::get_latest_prism_version().await? }; if version_from_log < latest_version { diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index 7eff42f..33d6193 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -41,14 +41,16 @@ pub async fn handle( return Ok(()); } - // detect PK users first to make sure we don't respond to unproxied messages - pluralkit::handle(ctx, new_message, data).await?; + if let Some(storage) = &data.storage { + // detect PK users first to make sure we don't respond to unproxied messages + pluralkit::handle(ctx, new_message, storage).await?; - if data.storage.is_user_plural(new_message.author.id).await? - && pluralkit::is_message_proxied(new_message).await? - { - debug!("Not replying to unproxied PluralKit message"); - return Ok(()); + if storage.is_user_plural(new_message.author.id).await? + && pluralkit::is_message_proxied(new_message).await? + { + debug!("Not replying to unproxied PluralKit message"); + return Ok(()); + } } eta::handle(ctx, new_message).await?; diff --git a/src/handlers/event/pluralkit.rs b/src/handlers/event/pluralkit.rs index ee8977a..27bdba1 100644 --- a/src/handlers/event/pluralkit.rs +++ b/src/handlers/event/pluralkit.rs @@ -1,4 +1,4 @@ -use crate::{api, Data}; +use crate::{api, storage::Storage}; use std::time::Duration; use eyre::Result; @@ -20,7 +20,7 @@ pub async fn is_message_proxied(message: &Message) -> Result { Ok(proxied) } -pub async fn handle(_: &Context, msg: &Message, data: &Data) -> Result<()> { +pub async fn handle(_: &Context, msg: &Message, storage: &Storage) -> Result<()> { if msg.webhook_id.is_none() { return Ok(()); } @@ -37,7 +37,7 @@ pub async fn handle(_: &Context, msg: &Message, data: &Data) -> Result<()> { sleep(PK_DELAY).await; if let Ok(sender) = api::pluralkit::get_sender(msg.id).await { - data.storage.store_user_plurality(sender).await?; + storage.store_user_plurality(sender).await?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index 93d7e5b..893250d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,16 +4,14 @@ use std::{sync::Arc, time::Duration}; -use eyre::{eyre, Context as _, Report, Result}; +use eyre::{bail, Context as _, Report, Result}; use log::{info, trace, warn}; -use octocrab::Octocrab; use poise::{ serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, }; use owo_colors::OwoColorize; -use redis::ConnectionLike; use tokio::signal::ctrl_c; #[cfg(target_family = "unix")] @@ -38,17 +36,13 @@ type Context<'a> = poise::Context<'a, Data, Report>; #[derive(Clone)] pub struct Data { config: Config, - storage: Storage, - octocrab: Arc, + storage: Option, } impl Data { - pub fn new(config: Config, storage: Storage, octocrab: Arc) -> Result { - Ok(Self { - config, - storage, - octocrab, - }) + #[must_use] + pub fn new(config: Config, storage: Option) -> Self { + Self { config, storage } } } @@ -58,19 +52,22 @@ async fn setup( framework: &Framework, ) -> Result { let config = Config::new_from_env(); - let storage = Storage::from_url(&config.redis_url)?; - let octocrab = octocrab::instance(); - let data = Data::new(config, storage, octocrab)?; - // test redis connection - let mut client = data.storage.client().clone(); + let storage = if let Some(url) = &config.clone().bot_config().redis_url() { + Some(Storage::from_url(url)?) + } else { + None + }; - if !client.check_connection() { - return Err(eyre!( - "Couldn't connect to storage! Is your daemon running?" - )); + if let Some(storage) = storage.as_ref() { + if !storage.clone().has_connection() { + bail!("You specified a storage backend but there's no connection! Is it running?") + } + + trace!("Redis connection looks good!"); } - trace!("Redis connection looks good!"); + + let data = Data::new(config, storage); poise::builtins::register_globally(ctx, &framework.options().commands).await?; info!("Registered global commands!"); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index af2b5f5..4d53855 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -3,10 +3,11 @@ use std::fmt::Debug; use eyre::Result; use log::debug; use poise::serenity_prelude::UserId; -use redis::{AsyncCommands, Client}; +use redis::{AsyncCommands, Client, ConnectionLike}; const PK_KEY: &str = "pluralkit-v1"; const LAUNCHER_VERSION_KEY: &str = "launcher-version-v1"; +const LAUNCHER_STARGAZER_KEY: &str = "launcher-stargazer-v1"; #[derive(Clone, Debug)] pub struct Storage { @@ -24,8 +25,8 @@ impl Storage { Ok(Self::new(client)) } - pub fn client(&self) -> &Client { - &self.client + pub fn has_connection(mut self) -> bool { + self.client.check_connection() } pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> { @@ -67,4 +68,23 @@ impl Storage { Ok(res) } + + pub async fn cache_launcher_stargazer_count(&self, stargazers: u32) -> Result<()> { + debug!("Caching stargazer count as {stargazers}"); + + let mut con = self.client.get_async_connection().await?; + con.set_ex(LAUNCHER_STARGAZER_KEY, stargazers, 60 * 60) + .await?; + + Ok(()) + } + + pub async fn get_launcher_stargazer_count(&self) -> Result { + debug!("Fetching launcher stargazer count"); + + let mut con = self.client.get_async_connection().await?; + let res: u32 = con.get(LAUNCHER_STARGAZER_KEY).await?; + + Ok(res) + } }