summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNathan Ringo <nathan@remexre.com>2024-11-22 00:11:43 -0600
committerNathan Ringo <nathan@remexre.com>2024-11-22 00:11:43 -0600
commitdd0e6e45c35133ec8a3e2886b7b050484b388d03 (patch)
tree3ea9f5a098fc9c150473942c97e8b07833440803 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/labwatch.rs67
-rw-r--r--src/lib.rs27
-rw-r--r--src/main.rs90
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(())
+}