/ src / matrix / commands / mod.rs
mod.rs
  1  use std::{borrow::Cow, sync::Arc};
  2  
  3  use matrix_sdk::{Room, ruma::events::room::message::OriginalRoomMessageEvent};
  4  
  5  use crate::{
  6      Context,
  7      aoc::day::AocDay,
  8      config::Config,
  9      matrix::utils::{RoomExt, error_message, message},
 10  };
 11  
 12  pub mod admin;
 13  pub mod aoc;
 14  mod parser;
 15  mod parser_ext;
 16  
 17  pub async fn handle(
 18      event: &OriginalRoomMessageEvent,
 19      room: Room,
 20      context: Arc<Context>,
 21      cmd: &str,
 22  ) -> anyhow::Result<()> {
 23      let cmd = parser::parse(cmd);
 24  
 25      if let Err(err) = match &*cmd.command {
 26          // Advent of Code
 27          "join" => aoc::join::invoke(event, &room, &context).await,
 28          "leaderboard" | "lb" => aoc::leaderboard::invoke(event, &room, &context, cmd).await,
 29          "day" => aoc::day::invoke(event, &room, &context, cmd).await,
 30          "user" => aoc::user::invoke(event, &room, &context, cmd).await,
 31          "solutions" | "repos" => aoc::solutions::invoke(event, &room, &context).await,
 32          "clear-cache" | "cc" => aoc::clear_cache::invoke(event, &room, &context).await,
 33  
 34          // General
 35          "ping" => ping(event, &room).await,
 36          "help" => Ok(help(event, &room, &context.config).await?),
 37  
 38          // Administration
 39          "op" => admin::op(event, &room, &context.config, cmd).await,
 40  
 41          _ => unknown_command(event, &room).await,
 42      } {
 43          err.send(&room, event).await?;
 44          if let CommandError::Other(err) = err {
 45              return Err(err);
 46          }
 47      }
 48  
 49      Ok(())
 50  }
 51  
 52  pub async fn help(
 53      event: &OriginalRoomMessageEvent,
 54      room: &Room,
 55      config: &Config,
 56  ) -> anyhow::Result<()> {
 57      let prefix = &config.matrix.command_prefix;
 58  
 59      let default_day = AocDay::current()
 60          .map(|d| format!("={}", d.day))
 61          .unwrap_or_default();
 62      let default_year = AocDay::most_recent().year;
 63      let default_rows = config.aoc.leaderboard_rows;
 64      let content = format!(
 65          r#"
 66  ### AoC-Bot Commands
 67  
 68  #### Advent of Code
 69  - `{prefix}join` - Request instructions to join the private leaderboard
 70  - `{prefix}leaderboard [year={default_year}] [rows={default_rows}] [offset=0]` - Show the given slice of the private leaderboard
 71  - `{prefix}day [day{default_day}] [year={default_year}] [p=1|2|both] [rows={default_rows}] [offset=0]` - Show the given slice of the daily private leaderboard
 72  - `{prefix}user [user] [year={default_year}]` - Show statistics of the given user
 73  - `{prefix}solutions` - Show the list of solution repositories
 74  - `{prefix}clear-cache` - Clear the leaderboard cache (admin only)
 75  
 76  #### General
 77  - `{prefix}ping` - Check bot health
 78  - `{prefix}help` - Show this help message
 79  "#
 80      );
 81  
 82      room.reply_to(event, message(content)).await?;
 83      Ok(())
 84  }
 85  
 86  async fn unknown_command(event: &OriginalRoomMessageEvent, room: &Room) -> CommandResult<()> {
 87      room.reply_to(
 88          event,
 89          error_message("Unknown command. Send `!help` for a list of available commands."),
 90      )
 91      .await?;
 92      Ok(())
 93  }
 94  
 95  pub async fn ping(event: &OriginalRoomMessageEvent, room: &Room) -> CommandResult<()> {
 96      room.reply_to(event, message("Pong!")).await?;
 97      Ok(())
 98  }
 99  
100  #[derive(Debug)]
101  pub enum CommandError {
102      ArgRequired(&'static str),
103      ArgParse {
104          arg: &'static str,
105          allowed_values: Option<Cow<'static, str>>,
106      },
107      PermissionDenied,
108      Other(anyhow::Error),
109  }
110  
111  type CommandResult<T> = Result<T, CommandError>;
112  
113  impl std::fmt::Display for CommandError {
114      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115          match self {
116              Self::ArgRequired(arg) => write!(f, "Argument '{arg}' is required")?,
117              Self::ArgParse {
118                  arg,
119                  allowed_values,
120              } => {
121                  write!(f, "Failed to parse argument '{arg}'.")?;
122                  if let Some(allowed_values) = allowed_values {
123                      write!(f, " Allowed values: {allowed_values}")?;
124                  }
125              }
126              Self::PermissionDenied => write!(f, "Permission denied")?,
127              Self::Other(_) => write!(f, "Internal error")?,
128          }
129  
130          Ok(())
131      }
132  }
133  
134  impl CommandError {
135      async fn send(&self, room: &Room, event: &OriginalRoomMessageEvent) -> anyhow::Result<()> {
136          room.reply_to(event, error_message(self.to_string()))
137              .await?;
138          Ok(())
139      }
140  }
141  
142  impl<E> From<E> for CommandError
143  where
144      E: Into<anyhow::Error>,
145  {
146      fn from(value: E) -> Self {
147          Self::Other(value.into())
148      }
149  }