diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/labwatch.rs | 67 | ||||
-rw-r--r-- | src/lib.rs | 27 | ||||
-rw-r--r-- | src/main.rs | 90 |
3 files changed, 184 insertions, 0 deletions
diff --git a/src/labwatch.rs b/src/labwatch.rs new file mode 100644 index 0000000..e2f8a7c --- /dev/null +++ b/src/labwatch.rs @@ -0,0 +1,67 @@ +use crate::HandlerConfig; +use anyhow::{anyhow, Context as _, Result}; +use serenity::all::{Context, MessageBuilder}; +use std::{sync::Arc, time::Duration}; +use time::{Date, OffsetDateTime, Weekday}; +use tokio::{ + fs::{self, create_dir_all, File}, + time::{interval, MissedTickBehavior}, +}; + +static REACTS: &[char] = &['✅', '❌', '❔']; + +pub async fn start(config: Arc<HandlerConfig>, ctx: Context) { + let mut timer = interval(Duration::from_secs(1)); + timer.set_missed_tick_behavior(MissedTickBehavior::Skip); + loop { + timer.tick().await; + if let Err(err) = tick(&*config, &ctx).await { + log::error!("{err:?}"); + } + } +} + +async fn tick(config: &HandlerConfig, ctx: &Context) -> Result<()> { + let now = OffsetDateTime::now_local().context("Failed to get current time")?; + if now.weekday() != Weekday::Sunday { + // Wait to post 'till Sunday. + return Ok(()); + } + let mut now_path = config.db_dir.join("labwatch"); + create_dir_all(&now_path) + .await + .with_context(|| anyhow!("Failed to create {now_path:?}"))?; + now_path.push(date_name(now.date())); + if fs::try_exists(&now_path) + .await + .with_context(|| anyhow!("Failed to check if {now_path:?} exists"))? + { + // Already posted this week. + return Ok(()); + } + + let mut date = now.date(); + for _ in 1..=5 { + date = date.next_day().context("Reached the end of times")?; + let msg = MessageBuilder::new().push(date_name(date)).build(); + let msg = config + .labwatch_channel_id + .say(&ctx.http, &msg) + .await + .with_context(|| anyhow!("Failed to send message for {date}"))?; + for &react in REACTS { + msg.react(&ctx.http, react).await.with_context(|| { + anyhow!("Failed to react with {react} on the message for {date}") + })?; + } + } + + File::create(&now_path) + .await + .with_context(|| anyhow!("Failed to create {now_path:?}"))?; + Ok(()) +} + +fn date_name(date: Date) -> String { + format!("{}, {} {}", date.weekday(), date.month(), date.day()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3b11a48 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,27 @@ +mod labwatch; + +use serenity::{ + all::{ChannelId, Context, EventHandler, Ready}, + async_trait, +}; +use std::{path::PathBuf, sync::Arc}; + +pub struct Handler(Arc<HandlerConfig>); + +impl From<HandlerConfig> for Handler { + fn from(config: HandlerConfig) -> Handler { + Handler(Arc::new(config)) + } +} + +pub struct HandlerConfig { + pub db_dir: PathBuf, + pub labwatch_channel_id: ChannelId, +} + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, _data_about_bot: Ready) { + tokio::spawn(labwatch::start(self.0.clone(), ctx)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ed10360 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::{value_parser, ArgAction, Parser}; +use log::LevelFilter; +use minneplbot::{Handler, HandlerConfig}; +use serenity::{ + all::{ChannelId, GatewayIntents}, + Client, +}; +use simple_logger::SimpleLogger; + +#[derive(Debug, Parser)] +struct Args { + /// Decreases the log level. + #[clap( + short, + long, + conflicts_with("verbose"), + action = ArgAction::Count, + value_parser = value_parser!(u8).range(..=3) + )] + quiet: u8, + + /// Increases the log level. + #[clap( + short, + long, + conflicts_with("quiet"), + action = ArgAction::Count, + value_parser = value_parser!(u8).range(..=2) + )] + verbose: u8, + + /// The directory to store files in. + #[clap(env = "DB_DIR")] + db_dir: PathBuf, + + /// The Discord API token. Pass this via an environment variable, _not_ the command-line! + #[clap( + long = "do-not-pass-the-discord-token-via-the-command-line", + env = "DISCORD_TOKEN" + )] + discord_token: String, + + /// The channel ID of the #labwatch channel. + #[clap(long = "labwatch-channel-id", env = "LABWATCH_CHANNEL_ID")] + labwatch_channel_id: ChannelId, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + // Parse the arguments. + let args = Args::parse(); + + // Compute the log level to use. + let log_level = match (args.quiet, args.verbose) { + (0, 0) => LevelFilter::Info, + (0, 1) => LevelFilter::Debug, + (0, 2) => LevelFilter::Trace, + (1, _) => LevelFilter::Warn, + (2, _) => LevelFilter::Error, + (_, _) => LevelFilter::Off, + }; + SimpleLogger::new() + .with_level(log_level) + .init() + .context("Failed to configure logger")?; + + // Promise that we aren't gonna call setenv. + unsafe { + use time::util::local_offset::{set_soundness, Soundness}; + set_soundness(Soundness::Unsound); + } + + // Start up the client. + let intents = GatewayIntents::default(); + Client::builder(&args.discord_token, intents) + .event_handler(Handler::from(HandlerConfig { + db_dir: args.db_dir, + labwatch_channel_id: args.labwatch_channel_id, + })) + .await + .context("failed to create Discord client")? + .start() + .await + .context("failed to start Discord client")?; + + Ok(()) +} |