From a82e9a1d1f2d9f9d5e244ded9a478738a937f766 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Tue, 10 Feb 2026 14:33:19 +0200 Subject: [PATCH] Add prek pre-commit hooks via mise, fix formatting and clippy warnings - Add mise.toml (prek 0.3.2) and .pre-commit-config.yaml with hooks for trailing whitespace, end-of-file, YAML check, merge conflicts, large files, cargo fmt, cargo clippy (-D warnings), and unit tests - Add prek/mise recipes to justfile - Run cargo fmt to fix formatting issues - Fix all clippy warnings: collapse nested if statements, derive Default for Avatar --- .pre-commit-config.yaml | 36 +++++++++++++++ justfile | 42 +++++++++++++++++- mise.toml | 6 +++ src/agent/provider/anthropic/controller.rs | 8 ++-- src/agent/provider/openai/controller.rs | 44 +++++++++---------- src/agent/provider/openai_compat/config.rs | 27 ++++++------ src/bot/implementation.rs | 2 +- .../access/room_local_agent_managers.rs | 22 +++++----- src/controller/access/users.rs | 22 +++++----- src/controller/cfg/dispatching/mod.rs | 24 +++++----- src/conversation/matrix/utils/mod.rs | 34 +++++++------- src/entity/cfg/config.rs | 23 ++++------ 12 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 mise.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ab943b6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + # Fast built-in hooks (Rust-native, no dependencies) + - repo: builtin + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-merge-conflict + - id: check-added-large-files + args: ['--maxkb=1024'] + + # Local hooks that run project-specific tools + - repo: local + hooks: + - id: cargo-fmt-check + name: Cargo Format Check + entry: cargo fmt --all -- --check + language: system + files: '\.rs$' + pass_filenames: false + + - id: cargo-clippy + name: Cargo Clippy + entry: cargo clippy -- -D warnings + language: system + files: '\.rs$' + pass_filenames: false + priority: 100 + + - id: test-unit + name: Unit Tests + entry: just test + language: system + files: '\.rs$' + pass_filenames: false + priority: 100 diff --git a/justfile b/justfile index a0e78b9..a86cde5 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,9 @@ project_name := "baibot" container_image_name := "localhost/baibot" project_container_network := "baibot" +mise_data_dir := env("MISE_DATA_DIR", justfile_directory() / "var/mise") +mise_trusted_config_paths := justfile_directory() / "mise.toml" + # Show help by default default: @just --list --justfile {{ justfile() }} @@ -159,6 +162,44 @@ synapse-register-regular-user username password: services-prepare clippy *extra_args: cargo clippy {{ extra_args }} +# Checks that the code compiles without building +check: + cargo check + +# Invokes mise with the project-local data directory +mise *args: _ensure_mise_data_directory + #!/bin/sh + export MISE_DATA_DIR="{{ mise_data_dir }}" + export MISE_TRUSTED_CONFIG_PATHS="{{ mise_trusted_config_paths }}" + mise {{ args }} + +# Runs prek (pre-commit hooks manager) with the given arguments +prek *args: _ensure_mise_tools_installed + @just --justfile {{ justfile() }} mise exec -- prek {{ args }} + +# Runs pre-commit hooks on staged files +prek-run-on-staged *args: _ensure_mise_tools_installed + @just --justfile {{ justfile() }} mise exec -- prek run {{ args }} + +# Runs pre-commit hooks on all files +prek-run-on-all *args: _ensure_mise_tools_installed + @just --justfile {{ justfile() }} mise exec -- prek run --all-files {{ args }} + +# Installs the git pre-commit hook (runs prek automatically before each commit) +prek-install-git-pre-commit-hook: _ensure_mise_tools_installed + @just --justfile {{ justfile() }} mise exec -- prek install + +# Internal - ensures var/mise directory exists +_ensure_mise_data_directory: + #!/bin/sh + if [ ! -d "{{ mise_data_dir }}" ]; then + mkdir -p "{{ mise_data_dir }}" + fi + +# Internal - ensures mise tools are installed +_ensure_mise_tools_installed: _ensure_mise_data_directory + @just --justfile {{ justfile() }} mise install --quiet + _prepare-var-services-env: #!/bin/sh cd {{ justfile_directory() }}; @@ -248,4 +289,3 @@ _prepare-var-app-container-data: if [ ! -f var/app/container/data ]; then mkdir -p var/app/container/data fi - diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..fe12230 --- /dev/null +++ b/mise.toml @@ -0,0 +1,6 @@ +[tools] +prek = "0.3.2" + +[settings] +# Disable automatic trust prompts - we trust this config +yes = true diff --git a/src/agent/provider/anthropic/controller.rs b/src/agent/provider/anthropic/controller.rs index fd46040..c5afcbc 100644 --- a/src/agent/provider/anthropic/controller.rs +++ b/src/agent/provider/anthropic/controller.rs @@ -146,10 +146,10 @@ impl ControllerTrait for Controller { .temperature_override .unwrap_or(text_generation_config.temperature); - if let Some(prompt_message) = prompt_message { - if let LLMMessageContent::Text(text) = &prompt_message.content { - request.system = text.clone(); - } + if let Some(prompt_message) = prompt_message + && let LLMMessageContent::Text(text) = &prompt_message.content + { + request.system = text.clone(); } request.model = text_generation_config.model_id.clone(); diff --git a/src/agent/provider/openai/controller.rs b/src/agent/provider/openai/controller.rs index f257b20..5860d41 100644 --- a/src/agent/provider/openai/controller.rs +++ b/src/agent/provider/openai/controller.rs @@ -5,14 +5,14 @@ use async_openai::{ config::OpenAIConfig, types::{ audio::{AudioInput, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs}, + images::{ + CreateImageEditRequestArgs, CreateImageRequestArgs, Image, ImageInput, ImageModel, + ImageResponseFormat, + }, responses::{ CodeInterpreterContainerAuto, CodeInterpreterTool, CodeInterpreterToolContainer, CreateResponseArgs, OutputItem, OutputMessageContent, Tool, WebSearchTool, }, - images::{ - CreateImageEditRequestArgs, CreateImageRequestArgs, - Image, ImageInput, ImageModel, ImageResponseFormat, - }, }, }; @@ -32,8 +32,8 @@ use crate::{ agent::{ AgentPurpose, provider::entity::{ - ImageEditResult, ImageGenerationResult, ImageSource, PingResult, - TextToSpeechParams, TextToSpeechResult, + ImageEditResult, ImageGenerationResult, ImageSource, PingResult, TextToSpeechParams, + TextToSpeechResult, }, }, strings, @@ -129,7 +129,8 @@ impl ControllerTrait for Controller { conversation_messages.insert(0, prompt_message); } - let input = super::utils::convert_llm_messages_to_openai_response_input(conversation_messages); + let input = + super::utils::convert_llm_messages_to_openai_response_input(conversation_messages); let messages_count = match &input { async_openai::types::responses::InputParam::Items(items) => items.len(), @@ -182,10 +183,7 @@ impl ControllerTrait for Controller { let response = self.client.responses().create(request).await?; - tracing::trace!( - ?response, - "Got response from the OpenAI response API" - ); + tracing::trace!(?response, "Got response from the OpenAI response API"); for item in response.output { if let OutputItem::Message(message) = item { @@ -271,9 +269,7 @@ impl ControllerTrait for Controller { ImageModel::GptImage1 => ImageModel::GptImage1Mini, ImageModel::GptImage1dot5 => ImageModel::GptImage1Mini, ImageModel::GptImage1Mini => ImageModel::GptImage1Mini, - ImageModel::Other(_) => { - ImageModel::DallE2 - } + ImageModel::Other(_) => ImageModel::DallE2, } } else { original_model @@ -408,9 +404,15 @@ impl ControllerTrait for Controller { } let dalle2_size = match image_generation_config.size { - Some(async_openai::types::images::ImageSize::S256x256) => Some(async_openai::types::images::ImageSize::S256x256), - Some(async_openai::types::images::ImageSize::S512x512) => Some(async_openai::types::images::ImageSize::S512x512), - Some(async_openai::types::images::ImageSize::S1024x1024) => Some(async_openai::types::images::ImageSize::S1024x1024), + Some(async_openai::types::images::ImageSize::S256x256) => { + Some(async_openai::types::images::ImageSize::S256x256) + } + Some(async_openai::types::images::ImageSize::S512x512) => { + Some(async_openai::types::images::ImageSize::S512x512) + } + Some(async_openai::types::images::ImageSize::S1024x1024) => { + Some(async_openai::types::images::ImageSize::S1024x1024) + } _ => None, }; @@ -419,12 +421,8 @@ impl ControllerTrait for Controller { .map_err(|err| anyhow::anyhow!(err))?; let response_format = match model.clone() { - ImageModel::DallE2 => { - Some(ImageResponseFormat::B64Json) - } - ImageModel::DallE3 => { - Some(ImageResponseFormat::B64Json) - } + ImageModel::DallE2 => Some(ImageResponseFormat::B64Json), + ImageModel::DallE3 => Some(ImageResponseFormat::B64Json), // gpt-image-1 only outputs base64 and we don't need to specify the response format. // In fact, specifying the response format results in an error. ImageModel::GptImage1 => None, diff --git a/src/agent/provider/openai_compat/config.rs b/src/agent/provider/openai_compat/config.rs index 22b94e7..57dd6a7 100644 --- a/src/agent/provider/openai_compat/config.rs +++ b/src/agent/provider/openai_compat/config.rs @@ -162,13 +162,14 @@ impl TryInto for TextToSpeechConfig { type Error = String; fn try_into(self) -> Result { - let model_id = convert_string_to_enum::(&self.model_id)?; + let model_id = + convert_string_to_enum::(&self.model_id)?; let voice = convert_string_to_enum::(&self.voice)?; - let response_format = convert_string_to_enum::( - &self.response_format, - )?; + let response_format = convert_string_to_enum::< + async_openai::types::audio::SpeechResponseFormat, + >(&self.response_format)?; Ok(OpenAITextToSpeechConfig { model_id, @@ -225,25 +226,25 @@ impl TryInto for ImageGenerationConfig { fn try_into(self) -> Result { let size = if let Some(size) = &self.size { - Some(convert_string_to_enum::( - size, - )?) + Some(convert_string_to_enum::< + async_openai::types::images::ImageSize, + >(size)?) } else { None }; let style = if let Some(style) = &self.style { - Some(convert_string_to_enum::( - style, - )?) + Some(convert_string_to_enum::< + async_openai::types::images::ImageStyle, + >(style)?) } else { None }; let quality = if let Some(quality) = &self.quality { - Some(convert_string_to_enum::( - quality, - )?) + Some(convert_string_to_enum::< + async_openai::types::images::ImageQuality, + >(quality)?) } else { None }; diff --git a/src/bot/implementation.rs b/src/bot/implementation.rs index 224d149..e85f7b9 100644 --- a/src/bot/implementation.rs +++ b/src/bot/implementation.rs @@ -4,10 +4,10 @@ use std::{future::Future, pin::Pin}; use mxlink::matrix_sdk::Room; use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters}; +use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName}; use mxlink::matrix_sdk::ruma::{ MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource, }; -use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName}; use mxlink::{ InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig, diff --git a/src/controller/access/room_local_agent_managers.rs b/src/controller/access/room_local_agent_managers.rs index a415888..66bc6a6 100644 --- a/src/controller/access/room_local_agent_managers.rs +++ b/src/controller/access/room_local_agent_managers.rs @@ -28,18 +28,18 @@ pub async fn handle_set( message_context: &MessageContext, patterns: &Option>, ) -> anyhow::Result<()> { - if let Some(patterns) = patterns { - if let Err(err) = mxidwc::parse_patterns_vector(patterns) { - bot.messaging() - .send_error_markdown_no_fail( - message_context.room(), - &strings::access::failed_to_parse_patterns(&err.to_string()), - MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), - ) - .await; + if let Some(patterns) = patterns + && let Err(err) = mxidwc::parse_patterns_vector(patterns) + { + bot.messaging() + .send_error_markdown_no_fail( + message_context.room(), + &strings::access::failed_to_parse_patterns(&err.to_string()), + MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), + ) + .await; - return Ok(()); - } + return Ok(()); } let mut global_config_manager_guard = bot.global_config_manager().lock().await; diff --git a/src/controller/access/users.rs b/src/controller/access/users.rs index fab8e14..a4021e7 100644 --- a/src/controller/access/users.rs +++ b/src/controller/access/users.rs @@ -24,18 +24,18 @@ pub async fn handle_set( message_context: &MessageContext, patterns: &Option>, ) -> anyhow::Result<()> { - if let Some(patterns) = patterns { - if let Err(err) = mxidwc::parse_patterns_vector(patterns) { - bot.messaging() - .send_error_markdown_no_fail( - message_context.room(), - &strings::access::failed_to_parse_patterns(&err.to_string()), - MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), - ) - .await; + if let Some(patterns) = patterns + && let Err(err) = mxidwc::parse_patterns_vector(patterns) + { + bot.messaging() + .send_error_markdown_no_fail( + message_context.room(), + &strings::access::failed_to_parse_patterns(&err.to_string()), + MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), + ) + .await; - return Ok(()); - } + return Ok(()); } let mut global_config_manager_guard = bot.global_config_manager().lock().await; diff --git a/src/controller/cfg/dispatching/mod.rs b/src/controller/cfg/dispatching/mod.rs index 3c5ca50..b278847 100644 --- a/src/controller/cfg/dispatching/mod.rs +++ b/src/controller/cfg/dispatching/mod.rs @@ -39,18 +39,18 @@ async fn dispatch_config_related_handler( message_context: &MessageContext, bot: &Bot, ) -> anyhow::Result<()> { - if let SettingsStorageSource::Global = config_type { - if !message_context.sender_can_manage_global_config() { - bot.messaging() - .send_error_markdown_no_fail( - message_context.room(), - strings::global_config::no_permissions_to_administrate(), - MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), - ) - .await; - return Ok(()); - } - }; + if let SettingsStorageSource::Global = config_type + && !message_context.sender_can_manage_global_config() + { + bot.messaging() + .send_error_markdown_no_fail( + message_context.room(), + strings::global_config::no_permissions_to_administrate(), + MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), + ) + .await; + return Ok(()); + } let room_settings = match config_type { SettingsStorageSource::Room => &message_context.room_config().settings, diff --git a/src/conversation/matrix/utils/mod.rs b/src/conversation/matrix/utils/mod.rs index 01ec0f3..ac5b3f5 100644 --- a/src/conversation/matrix/utils/mod.rs +++ b/src/conversation/matrix/utils/mod.rs @@ -154,35 +154,35 @@ pub async fn process_matrix_messages( let mut message = message.clone(); - if i == 0 && !params.first_message_prefixes_to_strip.is_empty() { - if let MatrixMessageContent::Text(message_text) = &message.content { - let mut message_text = message_text.clone(); + if i == 0 + && !params.first_message_prefixes_to_strip.is_empty() + && let MatrixMessageContent::Text(message_text) = &message.content + { + let mut message_text = message_text.clone(); - for prefix in ¶ms.first_message_prefixes_to_strip { - if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { - message_text = message_text_stripped.to_owned(); - } + for prefix in ¶ms.first_message_prefixes_to_strip { + if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { + message_text = message_text_stripped.to_owned(); } - - message.content = MatrixMessageContent::Text(message_text.trim().to_owned()); } + + message.content = MatrixMessageContent::Text(message_text.trim().to_owned()); } // We only strip `bot_user_prefixes_to_strip`-defined prefixes from messages that mention the bot user. if !params.bot_user_prefixes_to_strip.is_empty() && message.mentioned_users.contains(¶ms.bot_user_id) + && let MatrixMessageContent::Text(message_text) = &message.content { - if let MatrixMessageContent::Text(message_text) = &message.content { - let mut message_text = message_text.clone(); + let mut message_text = message_text.clone(); - for prefix in ¶ms.bot_user_prefixes_to_strip { - if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { - message_text = message_text_stripped.to_owned(); - } + for prefix in ¶ms.bot_user_prefixes_to_strip { + if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { + message_text = message_text_stripped.to_owned(); } - - message.content = MatrixMessageContent::Text(message_text.trim().to_owned()); } + + message.content = MatrixMessageContent::Text(message_text.trim().to_owned()); } messages_filtered.push(message); diff --git a/src/entity/cfg/config.rs b/src/entity/cfg/config.rs index 0e7d977..448c72d 100644 --- a/src/entity/cfg/config.rs +++ b/src/entity/cfg/config.rs @@ -88,9 +88,10 @@ impl ConfigHomeserver { /// - `Default`: Use the built-in default avatar (null, empty string, or missing in config) /// - `Keep`: Don't touch the avatar, keep whatever is already set ("keep" in config) /// - `Custom(String)`: Use a custom avatar from the specified file path -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize)] pub enum Avatar { /// Use the built-in default avatar + #[default] Default, /// Keep the current avatar, don't change it Keep, @@ -98,12 +99,6 @@ pub enum Avatar { Custom(String), } -impl Default for Avatar { - fn default() -> Self { - Avatar::Default - } -} - impl<'de> Deserialize<'de> for Avatar { fn deserialize(deserializer: D) -> Result where @@ -181,13 +176,13 @@ pub struct ConfigUserEncryption { impl ConfigUserEncryption { pub fn validate(&self) -> anyhow::Result<()> { - if let Some(passphrase) = &self.recovery_passphrase { - if passphrase.is_empty() { - return Err(anyhow::anyhow!( - "The user.encryption.recovery_passphrase ({}) configuration must either be null or set to a non-empty passphrase", - super::env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE - )); - } + if let Some(passphrase) = &self.recovery_passphrase + && passphrase.is_empty() + { + return Err(anyhow::anyhow!( + "The user.encryption.recovery_passphrase ({}) configuration must either be null or set to a non-empty passphrase", + super::env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE + )); } Ok(())