From c38cdc124f4109cece20e1766c4cacfca3b0be35 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 14:25:02 +0300 Subject: [PATCH 1/6] Centralize callback query message extraction to dispatcher Move message extraction from individual inline button handlers to the centralized dispatcher in bot.rs. This eliminates repetitive boilerplate code and simplifies handler signatures by passing Message directly. --- .claude/settings.local.json | 33 +++++++ CLAUDE.md | 95 +++++++++++++++++++ Justfile | 3 + Taskfile.yml | 1 + locales/analyze.yml | 34 +++---- locales/errors.yml | 4 + ...8152848_add_ai_slop_detection_counters.sql | 4 + src/telegram/actions/admin_users/details.rs | 17 +--- src/telegram/actions/admin_users/list.rs | 19 +--- src/telegram/actions/ai_slop_detection.rs | 20 +--- src/telegram/actions/analyze.rs | 22 +---- src/telegram/actions/dislike.rs | 15 +-- src/telegram/actions/ignore.rs | 15 +-- src/telegram/actions/magic.rs | 19 +--- src/telegram/actions/recommendasion.rs | 34 ++----- src/telegram/actions/skippage.rs | 23 +---- src/telegram/actions/song_links.rs | 22 ++--- src/telegram/actions/word_definition.rs | 27 +----- src/telegram/handlers/inline_buttons.rs | 30 +++--- src/workers/bot.rs | 39 +++++++- 20 files changed, 264 insertions(+), 212 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 migrations/20260308152848_add_ai_slop_detection_counters.sql diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..07d33524 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,33 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo check)", + "Bash(cargo clippy --all-targets --all-features)", + "Bash(cargo check --message-format=short)", + "Bash(cargo clippy --message-format=short)", + "Bash(cargo clippy --fix --allow-dirty)", + "Bash(tree:*)", + "Bash(cargo test:*)", + "Bash(cargo fmt:*)", + "Bash(cargo clippy:*)", + "mcp__github__search_code", + "mcp__github__search_pull_requests", + "mcp__github__list_pull_requests", + "Bash(gh run list:*)", + "Bash(cargo tree:*)", + "WebFetch(domain:github.com)", + "Bash(cargo build:*)", + "WebFetch(domain:docs.rs)", + "Bash(cargo search:*)", + "WebSearch", + "WebFetch(domain:crates.io)", + "Bash(cargo doc:*)", + "mcp__github__get_me", + "Bash(influx query:*)", + "Bash(git status:*)" + ], + "deny": [], + "ask": [] + }, + "disableAllHooks": true +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bcc50a40 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# Developer Guidelines + +## Code Quality Checks + +Run these at the end of every session: + +- `cargo fmt --all` - Format all code +- `cargo test` - Run tests and fix until they pass +- `cargo clippy --all-targets --all-features --no-deps -- -D warnings` - Fix all warnings +- Generate tests for new features you implement + +**Note**: Don't run `cargo check` separately - `cargo clippy` is sufficient as it includes all checks. + +## Tracing Guidelines + +Add `#[tracing::instrument(skip_all)]` to all public async functions: + +```rust +// Always include user_id when state is available +#[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] +pub async fn handle(app: &App, state: &UserState) -> anyhow::Result<()> { + +// Multiple fields - only include what's immediately available as parameters +#[tracing::instrument(skip_all, fields(%user_id, %track_id, ?status))] +pub async fn set_status(user_id: &str, track_id: &str, status: TrackStatus) -> anyhow::Result<()> { + +// Service methods with string parameters +#[tracing::instrument(skip_all, fields(%user_id, %track_id))] +pub async fn get_track(db: &impl ConnectionTrait, user_id: &str, track_id: &str) -> anyhow::Result { +``` + +**Keep it simple**: Only add fields available as function parameters, don't overcomplicate with computed values. + +## Code Style + +- Use `anyhow::Result` for all error handling +- Use `?` operator for error propagation +- State access: `state.user_id()`, `state.locale()`, `state.spotify().await` +- Localization: `t!("translation-key", locale = state.locale())` - translates keys from `locales/` directory +- Service pattern: `ServiceName::method_name(db, params)` + +## Naming Conventions + +**Project-Specific Feature Names:** + +- `recommendasion` - Intentional feature name for music recommendation functionality +- `skippage` - Intentional feature name (not a compound word or typo) + +## Testing + +- Add tests in `#[cfg(test)]` module at end of file +- Test business logic and edge cases +- Run `cargo test` to verify + +## Database Migrations + +To create a new migration: + +```bash +sqlx migrate add migration_name +``` + +This creates a new migration file in the `migrations/` directory. Write SQL queries for PostgreSQL using **only lowercase** (including keywords): + +```sql +-- Good +create table users ( + id bigint primary key, + name text not null +); + +-- Bad (don't use uppercase) +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + name TEXT NOT NULL +); +``` + +## Repository Information + +- **GitHub Owner:** `vtvz` +- **GitHub Repo:** `rustify` +- **Main Branch:** `master` + +## Common Patterns + +Quick reference for common operations: + +- Database: `app.db()` +- Redis: `app.redis_conn().await?` +- Spotify API: `state.spotify().await` +- Telegram Bot: `app.bot().send_message(...)` +- Current user: `state.user()`, `state.user_id()` +- Localization: `t!("key", locale = state.locale(), param = value)` +- GitHub Workflow: `gh workflow run --ref ` - when only one workflow exists, run it automatically for current branch diff --git a/Justfile b/Justfile index 4fa035ff..d02ce9a2 100644 --- a/Justfile +++ b/Justfile @@ -59,3 +59,6 @@ run: watch: cargo watch -s 'just run' + +lazysql: + lazysql 'postgresql://postgres:example@localhost:5432/postgres?sslmode=disable' diff --git a/Taskfile.yml b/Taskfile.yml index 4e06cf18..1fac7cfa 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -61,5 +61,6 @@ tasks: -i inventory/{{ .env }}/hosts.ini -e rustify_docker_image={{ .DOCKER_IMAGE_TAG | quote }} -t project + --check --diff playbook.yml diff --git a/locales/analyze.yml b/locales/analyze.yml index bb19a274..ae936ea0 100644 --- a/locales/analyze.yml +++ b/locales/analyze.yml @@ -6,33 +6,35 @@ analysis.word-analyzer-prompt: 1. Assume by default that the word is likely to be profane. 2. Censored profane words (with asterisks, symbols, or letter replacements) are still considered profane. 3. Always explain what it means or how it translates into English, considering it appears in song lyrics. - 4. If the word is profane, also classify its profaneness as one of: + 4. Some words have different meanings in different contexts, so account for that in your interpretation. + 5. If the word is profane, also classify its profaneness as one of: - normal word - mildly profane - highly profane - 5. If the word is not profane, mark it as normal word without mentioning offensiveness. - 6. The answer must be between 50 and 150 characters. - 7. The answer must be a single line, no line breaks. - 8. Do not include the given word itself or any other offensive words. - 9. Keep it clean and suitable for all audiences. - 10. Respond with no formatting. - 11. ONLY as a final check: if the word contains special characters (apostrophes, hyphens, etc.), extra or missing letters, or looks like a fragment of a larger word - AND it doesn't match any known profane words - then it may be a false positive from the profanity detection library. Mark such cases as normal word. + 6. If the word is not profane, mark it as normal word without mentioning offensiveness. + 7. The answer must be between 50 and 150 characters. + 8. The answer must be a single line, no line breaks. + 9. Do not include the given word itself or any other offensive words. + 10. Keep it clean and suitable for all audiences. + 11. Respond with no formatting. + 12. ONLY as a final check: if the word contains special characters (apostrophes, hyphens, etc.), extra or missing letters, or looks like a fragment of a larger word - AND it doesn't match any known profane words - then it may be a false positive from the profanity detection library. Mark such cases as normal word. ru: |- Тебе дано слово из песни: %{profane_word}. 1. По умолчанию предполагай, что слово скорее всего нецензурное. 2. Зацензуренные нецензурные слова (со звездочками, символами или заменами букв) все еще считаются нецензурными. 3. Всегда объясняй, что оно означает или как переводится на русский, учитывая, что оно встречается в тексте песни. - 4. Если слово нецензурное, также классифицируй его нецензурность как одну из: + 4. Некоторые слова имеют разные значения в разных контекстах, учитывай это при интерпретации. + 5. Если слово нецензурное, также классифицируй его нецензурность как одну из: - обычное слово - слегка нецензурное - сильно нецензурное - 5. Если слово не нецензурное, отметь его как обычное слово, не упоминая оскорбительность. - 6. Ответ должен быть от 50 до 150 символов. - 7. Ответ должен быть в одну строку, без переносов. - 8. Не включай само данное слово или любые другие оскорбительные слова. - 9. Делай ответ чистым и подходящим для всех аудиторий. - 10. Отвечай без форматирования. - 11. ТОЛЬКО в качестве финальной проверки: если слово содержит специальные символы (апострофы, дефисы и т.д.), лишние или недостающие буквы, или выглядит как фрагмент более крупного слова - И при этом не совпадает ни с одним известным нецензурным словом - тогда это может быть ложное срабатывание библиотеки обнаружения нецензурной лексики. Отметь такие случаи как обычное слово. + 6. Если слово не нецензурное, отметь его как обычное слово, не упоминая оскорбительность. + 7. Ответ должен быть от 50 до 150 символов. + 8. Ответ должен быть в одну строку, без переносов. + 9. Не включай само данное слово или любые другие оскорбительные слова. + 10. Делай ответ чистым и подходящим для всех аудиторий. + 11. Отвечай без форматирования. + 12. ТОЛЬКО в качестве финальной проверки: если слово содержит специальные символы (апострофы, дефисы и т.д.), лишние или недостающие буквы, или выглядит как фрагмент более крупного слова - И при этом не совпадает ни с одним известным нецензурным словом - тогда это может быть ложное срабатывание библиотеки обнаружения нецензурной лексики. Отметь такие случаи как обычное слово. analysis.prompt: en: |- diff --git a/locales/errors.yml b/locales/errors.yml index 53c83919..82a3780b 100644 --- a/locales/errors.yml +++ b/locales/errors.yml @@ -40,6 +40,10 @@ error.general: Сообщить о проблеме на GitHub +error.inaccessible-message: + en: |- + This message is too old or no longer accessible + error.unhandled-request: en: |- 😔 Your request was not handled diff --git a/migrations/20260308152848_add_ai_slop_detection_counters.sql b/migrations/20260308152848_add_ai_slop_detection_counters.sql new file mode 100644 index 00000000..a191d91b --- /dev/null +++ b/migrations/20260308152848_add_ai_slop_detection_counters.sql @@ -0,0 +1,4 @@ +alter table "user" + add column ai_slop_spotify_ai_blocker bigint not null default 0, + add column ai_slop_soul_over_ai bigint not null default 0, + add column ai_slop_shlabs bigint not null default 0; diff --git a/src/telegram/actions/admin_users/details.rs b/src/telegram/actions/admin_users/details.rs index 3e8937df..5871fd82 100644 --- a/src/telegram/actions/admin_users/details.rs +++ b/src/telegram/actions/admin_users/details.rs @@ -19,7 +19,6 @@ use crate::telegram::inline_buttons_admin::{ }; use crate::user::UserState; use crate::utils::DurationPrettyFormat as _; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id(), target_user_id = %user_id))] pub async fn handle_command( @@ -53,21 +52,14 @@ pub async fn handle_command( pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, user_id: String, page: u64, sort_by: AdminUsersSortBy, sort_order: AdminUsersSortOrder, status_filter: Option, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - return Ok(()); - }; - let text = format_user_details(app, &user_id).await?; let keyboard = InlineKeyboardMarkup::new(vec![vec![ @@ -80,10 +72,7 @@ pub async fn handle_inline( .into_inline_keyboard_button(state.locale()), ]]); - app.bot() - .edit_text(&message, text) - .reply_markup(keyboard) - .await?; + app.bot().edit_text(&m, text).reply_markup(keyboard).await?; Ok(()) } diff --git a/src/telegram/actions/admin_users/list.rs b/src/telegram/actions/admin_users/list.rs index 76c4aae6..c84bb682 100644 --- a/src/telegram/actions/admin_users/list.rs +++ b/src/telegram/actions/admin_users/list.rs @@ -16,7 +16,6 @@ use crate::telegram::inline_buttons_admin::{ AdminUsersSortOrder, }; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; const USERS_PER_PAGE: u64 = 10; @@ -45,31 +44,21 @@ pub async fn handle_command( } #[tracing::instrument(skip_all, fields(user_id = %state.user_id(), %page, ?sort_by, ?sort_order, ?status_filter))] +#[allow(clippy::too_many_arguments)] pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, page: u64, sort_by: AdminUsersSortBy, sort_order: AdminUsersSortOrder, status_filter: Option, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - let (text, keyboard) = build_users_page(app, state, page, sort_by, sort_order, status_filter).await?; - app.bot() - .edit_text(&message, text) - .reply_markup(keyboard) - .await?; + app.bot().edit_text(&m, text).reply_markup(keyboard).await?; Ok(()) } diff --git a/src/telegram/actions/ai_slop_detection.rs b/src/telegram/actions/ai_slop_detection.rs index 9174b0df..22bf3d71 100644 --- a/src/telegram/actions/ai_slop_detection.rs +++ b/src/telegram/actions/ai_slop_detection.rs @@ -1,9 +1,5 @@ use sea_orm::Iterable as _; -use teloxide::payloads::{ - AnswerCallbackQuerySetters as _, - EditMessageReplyMarkupSetters as _, - SendMessageSetters as _, -}; +use teloxide::payloads::{EditMessageReplyMarkupSetters as _, SendMessageSetters as _}; use teloxide::prelude::Requester as _; use teloxide::sugar::bot::BotMessagesExt as _; use teloxide::types::{ @@ -11,6 +7,7 @@ use teloxide::types::{ ChatId, InlineKeyboardButton, InlineKeyboardMarkup, + Message, ReplyMarkup, }; @@ -20,31 +17,22 @@ use crate::services::UserService; use crate::telegram::handlers::HandleStatus; use crate::telegram::inline_buttons::InlineButtons; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] pub async fn handle_inline( app: &'static App, state: &UserState, q: CallbackQuery, + m: Message, status: UserAISlopDetection, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - app.bot().answer_callback_query(q.id).await?; if status != state.user().cfg_ai_slop_detection { UserService::set_cfg_ai_slop_detection(app.db(), state.user_id(), status).await?; app.bot() - .edit_reply_markup(&message) + .edit_reply_markup(&m) .reply_markup(InlineKeyboardMarkup::new(get_keyboard( status, state.locale(), diff --git a/src/telegram/actions/analyze.rs b/src/telegram/actions/analyze.rs index fdd4a22b..18aaf83c 100644 --- a/src/telegram/actions/analyze.rs +++ b/src/telegram/actions/analyze.rs @@ -12,7 +12,7 @@ use teloxide::payloads::{ }; use teloxide::prelude::Requester as _; use teloxide::sugar::bot::BotMessagesExt as _; -use teloxide::types::{CallbackQuery, InlineKeyboardMarkup}; +use teloxide::types::{CallbackQuery, InlineKeyboardMarkup, Message}; use crate::app::{AIConfig, App}; use crate::lyrics::SearchResult as _; @@ -31,7 +31,6 @@ use crate::telegram::MESSAGE_MAX_LEN; use crate::telegram::inline_buttons::InlineButtons; use crate::telegram::utils::link_preview_small_top; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; use crate::utils::{DurationPrettyFormat as _, StringUtils as _}; #[tracing::instrument(skip_all, fields(user_id = %state.user_id(), %track_id))] @@ -39,17 +38,9 @@ pub async fn handle_inline( app: &'static App, state: &UserState, q: CallbackQuery, + m: Message, track_id: &str, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - let Some(config) = app.ai() else { app.bot() .answer_callback_query(q.id) @@ -91,10 +82,7 @@ pub async fn handle_inline( .await? else { app.bot() - .edit_text( - &message, - t!("analysis.lyrics-not-found", locale = state.locale()), - ) + .edit_text(&m, t!("analysis.lyrics-not-found", locale = state.locale())) .await?; return Ok(()); @@ -102,7 +90,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "analysis.waiting", locale = state.locale(), @@ -116,7 +104,7 @@ pub async fn handle_inline( let res = perform(app, state, config, &track, &hit.lyrics()).await; // I don't care about error - app.bot().delete(&message).await.ok(); + app.bot().delete(&m).await.ok(); match res { Ok(()) => { diff --git a/src/telegram/actions/dislike.rs b/src/telegram/actions/dislike.rs index c2ffee30..75429f42 100644 --- a/src/telegram/actions/dislike.rs +++ b/src/telegram/actions/dislike.rs @@ -13,7 +13,6 @@ use crate::telegram::handlers::HandleStatus; use crate::telegram::utils::link_preview_small_top; use crate::user::UserState; use crate::utils::DurationPrettyFormat as _; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] pub async fn handle( @@ -80,7 +79,8 @@ pub async fn handle( pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, track_id: &str, ) -> anyhow::Result<()> { let track = state @@ -95,17 +95,8 @@ pub async fn handle_inline( let keyboard = InlineButtons::from_track_status(TrackStatus::Disliked, track.id(), state.locale()); - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - app.bot() - .edit_text(&message, compose_message_text(&track, state.locale())) + .edit_text(&m, compose_message_text(&track, state.locale())) .link_preview_options(link_preview_small_top(track.url())) .reply_markup(InlineKeyboardMarkup::new(keyboard)) .await?; diff --git a/src/telegram/actions/ignore.rs b/src/telegram/actions/ignore.rs index 136ef319..a53d3745 100644 --- a/src/telegram/actions/ignore.rs +++ b/src/telegram/actions/ignore.rs @@ -9,24 +9,15 @@ use crate::entity::prelude::*; use crate::services::TrackStatusService; use crate::telegram::utils::link_preview_small_top; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id(), %track_id))] pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, track_id: &str, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - let track = state .spotify() .await @@ -41,7 +32,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "actions.ignore", track_link = track.track_tg_link(), diff --git a/src/telegram/actions/magic.rs b/src/telegram/actions/magic.rs index 526036b5..8dd678c4 100644 --- a/src/telegram/actions/magic.rs +++ b/src/telegram/actions/magic.rs @@ -9,7 +9,7 @@ use teloxide::payloads::{ }; use teloxide::prelude::Requester as _; use teloxide::sugar::bot::BotMessagesExt as _; -use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, ReplyMarkup}; +use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, Message, ReplyMarkup}; use crate::app::App; use crate::services::{RateLimitAction, RateLimitOutput, RateLimitService, UserService}; @@ -20,7 +20,6 @@ use crate::telegram::inline_buttons::InlineButtons; use crate::telegram::utils::link_preview_small_top; use crate::user::UserState; use crate::utils::DurationPrettyFormat as _; -use crate::utils::teloxide::CallbackQueryExt as _; #[allow(clippy::significant_drop_tightening)] #[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] @@ -66,16 +65,8 @@ pub async fn handle_inline( app: &'static App, state: &UserState, q: CallbackQuery, + m: Message, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - if !state.is_spotify_authed().await { actions::login::send_login_invite(app, state).await?; @@ -109,7 +100,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!("magic.generating", header = header, locale = state.locale()), ) .await?; @@ -120,7 +111,7 @@ pub async fn handle_inline( Ok(playlist) => { app.bot() .edit_text( - &message, + &m, t!( "magic.generated", header = header, @@ -137,7 +128,7 @@ pub async fn handle_inline( Err(err) => { app.bot() .edit_text( - &message, + &m, t!("magic.failed", header = header, locale = state.locale()), ) .await?; diff --git a/src/telegram/actions/recommendasion.rs b/src/telegram/actions/recommendasion.rs index 0fda9b06..ed923eb5 100644 --- a/src/telegram/actions/recommendasion.rs +++ b/src/telegram/actions/recommendasion.rs @@ -27,7 +27,7 @@ use teloxide::payloads::{ use teloxide::prelude::Requester as _; use teloxide::sugar::bot::BotMessagesExt as _; use teloxide::sugar::request::RequestLinkPreviewExt as _; -use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, ReplyMarkup}; +use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, Message, ReplyMarkup}; use crate::app::{AIConfig, App}; use crate::entity::prelude::TrackStatus; @@ -43,7 +43,6 @@ use crate::telegram::actions; use crate::telegram::handlers::HandleStatus; use crate::telegram::inline_buttons::InlineButtons; use crate::user::{SpotifyWrapperType, UserState}; -use crate::utils::teloxide::CallbackQueryExt as _; use crate::utils::{DurationPrettyFormat as _, StringUtils as _}; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -152,16 +151,8 @@ pub async fn handle_inline( app: &'static App, state: &UserState, q: CallbackQuery, + m: Message, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - if !state.is_spotify_authed().await { actions::login::send_login_invite(app, state).await?; @@ -170,10 +161,7 @@ pub async fn handle_inline( let Some(config) = app.ai() else { app.bot() - .edit_text( - &message, - t!("recommendasion.disabled", locale = state.locale()), - ) + .edit_text(&m, t!("recommendasion.disabled", locale = state.locale())) .await?; return Ok(()); @@ -191,7 +179,7 @@ pub async fn handle_inline( else { app.bot() .edit_text( - &message, + &m, t!("recommendasion.device-not-found", locale = state.locale()), ) .reply_markup(InlineKeyboardMarkup::new(vec![vec![ @@ -224,7 +212,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "recommendasion.collecting-favorites", locale = state.locale() @@ -243,10 +231,7 @@ pub async fn handle_inline( }; app.bot() - .edit_text( - &message, - t!("recommendasion.ask-ai", locale = state.locale()), - ) + .edit_text(&m, t!("recommendasion.ask-ai", locale = state.locale())) .await?; let recommendations = get_recommendations(app, state, config, &mut user_data).await?; @@ -255,10 +240,7 @@ pub async fn handle_inline( / (recommendations.slop.len() + recommendations.recommended.len() + 1); app.bot() - .edit_text( - &message, - t!("recommendasion.queue", locale = state.locale()), - ) + .edit_text(&m, t!("recommendasion.queue", locale = state.locale())) .await?; let mut recommendation_links = vec![]; @@ -294,7 +276,7 @@ pub async fn handle_inline( ); app.bot() - .edit_text(&message, text) + .edit_text(&m, text) .disable_link_preview(true) .reply_markup(InlineKeyboardMarkup::new(vec![vec![ InlineButtons::Recommendasion.into_inline_keyboard_button(state.locale()), diff --git a/src/telegram/actions/skippage.rs b/src/telegram/actions/skippage.rs index bb24a4b2..1b7217d6 100644 --- a/src/telegram/actions/skippage.rs +++ b/src/telegram/actions/skippage.rs @@ -1,12 +1,8 @@ use chrono::Duration; -use teloxide::payloads::{ - AnswerCallbackQuerySetters as _, - EditMessageTextSetters as _, - SendMessageSetters as _, -}; +use teloxide::payloads::{EditMessageTextSetters as _, SendMessageSetters as _}; use teloxide::prelude::Requester as _; use teloxide::sugar::bot::BotMessagesExt as _; -use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, ReplyMarkup}; +use teloxide::types::{CallbackQuery, ChatId, InlineKeyboardMarkup, Message, ReplyMarkup}; use crate::app::App; use crate::services::{SkippageService, UserService}; @@ -15,24 +11,15 @@ use crate::telegram::commands::UserCommandDisplay; use crate::telegram::handlers::HandleStatus; use crate::telegram::inline_buttons::InlineButtons; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, to_enable: bool, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - UserService::set_cfg_skippage_enabled(app.db(), state.user_id(), to_enable).await?; let days = Duration::seconds(state.user().cfg_skippage_secs).num_days(); @@ -48,7 +35,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "skippage.main", locale = state.locale(), diff --git a/src/telegram/actions/song_links.rs b/src/telegram/actions/song_links.rs index b42a7027..987120fb 100644 --- a/src/telegram/actions/song_links.rs +++ b/src/telegram/actions/song_links.rs @@ -1,31 +1,21 @@ use itertools::Itertools as _; use rspotify::model::TrackId; -use teloxide::payloads::{AnswerCallbackQuerySetters as _, EditMessageTextSetters as _}; -use teloxide::prelude::Requester as _; +use teloxide::payloads::EditMessageTextSetters as _; use teloxide::sugar::bot::BotMessagesExt as _; -use teloxide::types::CallbackQuery; +use teloxide::types::{CallbackQuery, Message}; use crate::app::App; use crate::telegram::utils::link_preview_small_top; use crate::user::UserState; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(user_id = %state.user_id(), %track_id))] pub async fn handle_inline( app: &'static App, state: &UserState, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, track_id: &str, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - let mut redis_conn = app.redis_conn().await?; let track = state @@ -36,7 +26,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "song-links.fetch", track_name = track.track_tg_link(), @@ -58,7 +48,7 @@ pub async fn handle_inline( app.bot() .edit_text( - &message, + &m, t!( "song-links.result", track_name = track.track_tg_link(), diff --git a/src/telegram/actions/word_definition.rs b/src/telegram/actions/word_definition.rs index 2b0ad607..cb5e74e3 100644 --- a/src/telegram/actions/word_definition.rs +++ b/src/telegram/actions/word_definition.rs @@ -9,7 +9,6 @@ use crate::services::{WordDefinitionService, WordStatsService}; use crate::telegram::commands_admin::AdminCommandDisplay; use crate::telegram::handlers::HandleStatus; use crate::telegram::inline_buttons_admin::AdminInlineButtons; -use crate::utils::teloxide::CallbackQueryExt as _; #[tracing::instrument(skip_all, fields(%locale, %word))] async fn generate_and_send_definition( @@ -100,20 +99,12 @@ pub async fn handle_definition( #[tracing::instrument(skip_all, fields(%locale, %word))] pub async fn handle_inline_regenerate( app: &'static App, - q: CallbackQuery, + _q: CallbackQuery, + m: Message, locale: String, word: String, ) -> anyhow::Result<()> { - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - - generate_and_send_definition(app, &message, locale, word, true).await?; + generate_and_send_definition(app, &m, locale, word, true).await?; Ok(()) } @@ -133,21 +124,13 @@ pub async fn handle_list( pub async fn handle_inline_list( app: &'static App, q: CallbackQuery, + m: Message, locale_filter: String, page: usize, ) -> anyhow::Result<()> { app.bot().answer_callback_query(q.id.clone()).await?; - let Some(message) = q.get_message() else { - app.bot() - .answer_callback_query(q.id.clone()) - .text("Inaccessible Message") - .await?; - - return Ok(()); - }; - - send_definitions_page(app, message.chat.id, Some(message), locale_filter, page).await?; + send_definitions_page(app, m.chat.id, Some(m), locale_filter, page).await?; Ok(()) } diff --git a/src/telegram/handlers/inline_buttons.rs b/src/telegram/handlers/inline_buttons.rs index 5e871a75..e68a4434 100644 --- a/src/telegram/handlers/inline_buttons.rs +++ b/src/telegram/handlers/inline_buttons.rs @@ -13,7 +13,12 @@ use crate::user::UserState; user_id = %state.user_id(), ) )] -pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> anyhow::Result<()> { +pub async fn handle( + app: &'static App, + state: &UserState, + q: CallbackQuery, + m: Message, +) -> anyhow::Result<()> { let data = q.data.as_ref().context("Callback needs data")?; let admin_button: Result = data.parse(); @@ -31,10 +36,10 @@ pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> a match button { AdminInlineButtons::RegenerateWordDefinition { locale, word } => { - actions::word_definition::handle_inline_regenerate(app, q, locale, word).await?; + actions::word_definition::handle_inline_regenerate(app, q, m, locale, word).await?; }, AdminInlineButtons::WordDefinitionsPage { locale, page, .. } => { - actions::word_definition::handle_inline_list(app, q, locale, page).await?; + actions::word_definition::handle_inline_list(app, q, m, locale, page).await?; }, AdminInlineButtons::AdminUserSelect { user_id, @@ -48,6 +53,7 @@ pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> a app, state, q, + m, user_id, page, sort_by, @@ -66,6 +72,7 @@ pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> a app, state, q, + m, page, sort_by, sort_order, @@ -88,6 +95,7 @@ pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> a app, state, q, + m, page, sort_by, sort_order, @@ -131,28 +139,28 @@ pub async fn handle(app: &'static App, state: &UserState, q: CallbackQuery) -> a match button { InlineButtons::Dislike(id) => { - actions::dislike::handle_inline(app, state, q, &id).await?; + actions::dislike::handle_inline(app, state, q, m, &id).await?; }, InlineButtons::Ignore(id) => { - actions::ignore::handle_inline(app, state, q, &id).await?; + actions::ignore::handle_inline(app, state, q, m, &id).await?; }, InlineButtons::Analyze(id) => { - actions::analyze::handle_inline(app, state, q, &id).await?; + actions::analyze::handle_inline(app, state, q, m, &id).await?; }, InlineButtons::SongLinks(id) => { - actions::song_links::handle_inline(app, state, q, &id).await?; + actions::song_links::handle_inline(app, state, q, m, &id).await?; }, InlineButtons::Magic => { - actions::magic::handle_inline(app, state, q).await?; + actions::magic::handle_inline(app, state, q, m).await?; }, InlineButtons::Recommendasion => { - actions::recommendasion::handle_inline(app, state, q).await?; + actions::recommendasion::handle_inline(app, state, q, m).await?; }, InlineButtons::SkippageEnable(to_enable) => { - actions::skippage::handle_inline(app, state, q, to_enable).await?; + actions::skippage::handle_inline(app, state, q, m, to_enable).await?; }, InlineButtons::AISlopDetection(status, _) => { - actions::ai_slop_detection::handle_inline(app, state, q, status).await?; + actions::ai_slop_detection::handle_inline(app, state, q, m, status).await?; }, } diff --git a/src/workers/bot.rs b/src/workers/bot.rs index 9cf043d4..775b074b 100644 --- a/src/workers/bot.rs +++ b/src/workers/bot.rs @@ -10,6 +10,7 @@ use crate::infrastructure::error_handler; use crate::services::UserService; use crate::telegram::commands::UserCommand; use crate::user::UserState; +use crate::utils::teloxide::CallbackQueryExt as _; use crate::{self as rustify}; #[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] @@ -105,14 +106,46 @@ pub async fn work() { } } - Ok(()) + anyhow::Ok(()) }), ) .branch(Update::filter_callback_query().endpoint( move |q: CallbackQuery| async { - let state = app.user_state(&q.from.id.to_string()).await?; + let Some(m) = q.get_message() else { + app.bot() + .answer_callback_query(q.id.clone()) + .text(t!("error.inaccessible-message", locale = "en")) + .show_alert(true) + .await?; + + return Ok(()); + }; + + let chat_id = m.chat.id; - rustify::telegram::handlers::inline_buttons::handle(app, &state, q).await + let state = app.user_state(&chat_id.to_string()).await?; + + let result = Box::pin(rustify::telegram::handlers::inline_buttons::handle(app, &state, q, m)).await; + + if let Err(mut err) = result { + let res = error_handler::handle(&mut err, app, state.user_id(), state.locale()).await; + if !res.user_notified { + app.bot().send_message( + chat_id, + formatdoc!( + r#" + Sorry, error has happened :( + + Report an issue on GitHub + "# + ) + ) + .disable_link_preview(true) + .await?; + } + } + + Ok(()) }, )); From 6568bcb1775ac78bf1d91927f3c778966fc659f2 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 14:33:44 +0300 Subject: [PATCH 2/6] rollback --- .claude/settings.local.json | 33 ------- CLAUDE.md | 95 ------------------- Taskfile.yml | 1 - ...8152848_add_ai_slop_detection_counters.sql | 4 - 4 files changed, 133 deletions(-) delete mode 100644 .claude/settings.local.json delete mode 100644 CLAUDE.md delete mode 100644 migrations/20260308152848_add_ai_slop_detection_counters.sql diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 07d33524..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cargo check)", - "Bash(cargo clippy --all-targets --all-features)", - "Bash(cargo check --message-format=short)", - "Bash(cargo clippy --message-format=short)", - "Bash(cargo clippy --fix --allow-dirty)", - "Bash(tree:*)", - "Bash(cargo test:*)", - "Bash(cargo fmt:*)", - "Bash(cargo clippy:*)", - "mcp__github__search_code", - "mcp__github__search_pull_requests", - "mcp__github__list_pull_requests", - "Bash(gh run list:*)", - "Bash(cargo tree:*)", - "WebFetch(domain:github.com)", - "Bash(cargo build:*)", - "WebFetch(domain:docs.rs)", - "Bash(cargo search:*)", - "WebSearch", - "WebFetch(domain:crates.io)", - "Bash(cargo doc:*)", - "mcp__github__get_me", - "Bash(influx query:*)", - "Bash(git status:*)" - ], - "deny": [], - "ask": [] - }, - "disableAllHooks": true -} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index bcc50a40..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,95 +0,0 @@ -# Developer Guidelines - -## Code Quality Checks - -Run these at the end of every session: - -- `cargo fmt --all` - Format all code -- `cargo test` - Run tests and fix until they pass -- `cargo clippy --all-targets --all-features --no-deps -- -D warnings` - Fix all warnings -- Generate tests for new features you implement - -**Note**: Don't run `cargo check` separately - `cargo clippy` is sufficient as it includes all checks. - -## Tracing Guidelines - -Add `#[tracing::instrument(skip_all)]` to all public async functions: - -```rust -// Always include user_id when state is available -#[tracing::instrument(skip_all, fields(user_id = %state.user_id()))] -pub async fn handle(app: &App, state: &UserState) -> anyhow::Result<()> { - -// Multiple fields - only include what's immediately available as parameters -#[tracing::instrument(skip_all, fields(%user_id, %track_id, ?status))] -pub async fn set_status(user_id: &str, track_id: &str, status: TrackStatus) -> anyhow::Result<()> { - -// Service methods with string parameters -#[tracing::instrument(skip_all, fields(%user_id, %track_id))] -pub async fn get_track(db: &impl ConnectionTrait, user_id: &str, track_id: &str) -> anyhow::Result { -``` - -**Keep it simple**: Only add fields available as function parameters, don't overcomplicate with computed values. - -## Code Style - -- Use `anyhow::Result` for all error handling -- Use `?` operator for error propagation -- State access: `state.user_id()`, `state.locale()`, `state.spotify().await` -- Localization: `t!("translation-key", locale = state.locale())` - translates keys from `locales/` directory -- Service pattern: `ServiceName::method_name(db, params)` - -## Naming Conventions - -**Project-Specific Feature Names:** - -- `recommendasion` - Intentional feature name for music recommendation functionality -- `skippage` - Intentional feature name (not a compound word or typo) - -## Testing - -- Add tests in `#[cfg(test)]` module at end of file -- Test business logic and edge cases -- Run `cargo test` to verify - -## Database Migrations - -To create a new migration: - -```bash -sqlx migrate add migration_name -``` - -This creates a new migration file in the `migrations/` directory. Write SQL queries for PostgreSQL using **only lowercase** (including keywords): - -```sql --- Good -create table users ( - id bigint primary key, - name text not null -); - --- Bad (don't use uppercase) -CREATE TABLE users ( - id BIGINT PRIMARY KEY, - name TEXT NOT NULL -); -``` - -## Repository Information - -- **GitHub Owner:** `vtvz` -- **GitHub Repo:** `rustify` -- **Main Branch:** `master` - -## Common Patterns - -Quick reference for common operations: - -- Database: `app.db()` -- Redis: `app.redis_conn().await?` -- Spotify API: `state.spotify().await` -- Telegram Bot: `app.bot().send_message(...)` -- Current user: `state.user()`, `state.user_id()` -- Localization: `t!("key", locale = state.locale(), param = value)` -- GitHub Workflow: `gh workflow run --ref ` - when only one workflow exists, run it automatically for current branch diff --git a/Taskfile.yml b/Taskfile.yml index 1fac7cfa..4e06cf18 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -61,6 +61,5 @@ tasks: -i inventory/{{ .env }}/hosts.ini -e rustify_docker_image={{ .DOCKER_IMAGE_TAG | quote }} -t project - --check --diff playbook.yml diff --git a/migrations/20260308152848_add_ai_slop_detection_counters.sql b/migrations/20260308152848_add_ai_slop_detection_counters.sql deleted file mode 100644 index a191d91b..00000000 --- a/migrations/20260308152848_add_ai_slop_detection_counters.sql +++ /dev/null @@ -1,4 +0,0 @@ -alter table "user" - add column ai_slop_spotify_ai_blocker bigint not null default 0, - add column ai_slop_soul_over_ai bigint not null default 0, - add column ai_slop_shlabs bigint not null default 0; From 8f90e5cddbeafee01863fcd28f7fed0fed3f497b Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 14:34:17 +0300 Subject: [PATCH 3/6] rollback --- locales/analyze.yml | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/locales/analyze.yml b/locales/analyze.yml index ae936ea0..bb19a274 100644 --- a/locales/analyze.yml +++ b/locales/analyze.yml @@ -6,35 +6,33 @@ analysis.word-analyzer-prompt: 1. Assume by default that the word is likely to be profane. 2. Censored profane words (with asterisks, symbols, or letter replacements) are still considered profane. 3. Always explain what it means or how it translates into English, considering it appears in song lyrics. - 4. Some words have different meanings in different contexts, so account for that in your interpretation. - 5. If the word is profane, also classify its profaneness as one of: + 4. If the word is profane, also classify its profaneness as one of: - normal word - mildly profane - highly profane - 6. If the word is not profane, mark it as normal word without mentioning offensiveness. - 7. The answer must be between 50 and 150 characters. - 8. The answer must be a single line, no line breaks. - 9. Do not include the given word itself or any other offensive words. - 10. Keep it clean and suitable for all audiences. - 11. Respond with no formatting. - 12. ONLY as a final check: if the word contains special characters (apostrophes, hyphens, etc.), extra or missing letters, or looks like a fragment of a larger word - AND it doesn't match any known profane words - then it may be a false positive from the profanity detection library. Mark such cases as normal word. + 5. If the word is not profane, mark it as normal word without mentioning offensiveness. + 6. The answer must be between 50 and 150 characters. + 7. The answer must be a single line, no line breaks. + 8. Do not include the given word itself or any other offensive words. + 9. Keep it clean and suitable for all audiences. + 10. Respond with no formatting. + 11. ONLY as a final check: if the word contains special characters (apostrophes, hyphens, etc.), extra or missing letters, or looks like a fragment of a larger word - AND it doesn't match any known profane words - then it may be a false positive from the profanity detection library. Mark such cases as normal word. ru: |- Тебе дано слово из песни: %{profane_word}. 1. По умолчанию предполагай, что слово скорее всего нецензурное. 2. Зацензуренные нецензурные слова (со звездочками, символами или заменами букв) все еще считаются нецензурными. 3. Всегда объясняй, что оно означает или как переводится на русский, учитывая, что оно встречается в тексте песни. - 4. Некоторые слова имеют разные значения в разных контекстах, учитывай это при интерпретации. - 5. Если слово нецензурное, также классифицируй его нецензурность как одну из: + 4. Если слово нецензурное, также классифицируй его нецензурность как одну из: - обычное слово - слегка нецензурное - сильно нецензурное - 6. Если слово не нецензурное, отметь его как обычное слово, не упоминая оскорбительность. - 7. Ответ должен быть от 50 до 150 символов. - 8. Ответ должен быть в одну строку, без переносов. - 9. Не включай само данное слово или любые другие оскорбительные слова. - 10. Делай ответ чистым и подходящим для всех аудиторий. - 11. Отвечай без форматирования. - 12. ТОЛЬКО в качестве финальной проверки: если слово содержит специальные символы (апострофы, дефисы и т.д.), лишние или недостающие буквы, или выглядит как фрагмент более крупного слова - И при этом не совпадает ни с одним известным нецензурным словом - тогда это может быть ложное срабатывание библиотеки обнаружения нецензурной лексики. Отметь такие случаи как обычное слово. + 5. Если слово не нецензурное, отметь его как обычное слово, не упоминая оскорбительность. + 6. Ответ должен быть от 50 до 150 символов. + 7. Ответ должен быть в одну строку, без переносов. + 8. Не включай само данное слово или любые другие оскорбительные слова. + 9. Делай ответ чистым и подходящим для всех аудиторий. + 10. Отвечай без форматирования. + 11. ТОЛЬКО в качестве финальной проверки: если слово содержит специальные символы (апострофы, дефисы и т.д.), лишние или недостающие буквы, или выглядит как фрагмент более крупного слова - И при этом не совпадает ни с одним известным нецензурным словом - тогда это может быть ложное срабатывание библиотеки обнаружения нецензурной лексики. Отметь такие случаи как обычное слово. analysis.prompt: en: |- From 4a83b3763176c39d64f83847f9cd1716e9dc1c26 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 14:42:43 +0300 Subject: [PATCH 4/6] Replace hardcoded error messages with localized versions Use t!("error.general") instead of inline formatdoc! for consistency with the rest of the codebase. Add documentation comment explaining when callback query messages can be inaccessible. --- .gitignore | 1 + src/workers/bot.rs | 18 ++++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 4aefd7c7..ff0f10a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ src/entity.example /target .env .env.deploy +.claude diff --git a/src/workers/bot.rs b/src/workers/bot.rs index 775b074b..09afbd50 100644 --- a/src/workers/bot.rs +++ b/src/workers/bot.rs @@ -93,13 +93,7 @@ pub async fn work() { if !res.user_notified { app.bot().send_message( m.chat.id, - formatdoc!( - r#" - Sorry, error has happened :( - - Report an issue on GitHub - "# - ) + t!("error.general", locale = state.locale()) ) .disable_link_preview(true) .await?; @@ -111,6 +105,8 @@ pub async fn work() { ) .branch(Update::filter_callback_query().endpoint( move |q: CallbackQuery| async { + // Message can be None for: inline mode results, old messages (48+ hours), + // deleted messages, or inaccessible channels let Some(m) = q.get_message() else { app.bot() .answer_callback_query(q.id.clone()) @@ -132,13 +128,7 @@ pub async fn work() { if !res.user_notified { app.bot().send_message( chat_id, - formatdoc!( - r#" - Sorry, error has happened :( - - Report an issue on GitHub - "# - ) + t!("error.general", locale = state.locale()) ) .disable_link_preview(true) .await?; From 96fddc2db48f5b390536e1cf08e3ebe26dda4246 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 16:56:37 +0300 Subject: [PATCH 5/6] Remove unused formatdoc import No longer needed after switching error messages to use t!() macro and plain text for inaccessible message edge case. --- src/workers/bot.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/workers/bot.rs b/src/workers/bot.rs index 09afbd50..7f309153 100644 --- a/src/workers/bot.rs +++ b/src/workers/bot.rs @@ -1,4 +1,3 @@ -use indoc::formatdoc; use sea_orm::Iterable as _; use teloxide::prelude::*; use teloxide::sugar::request::RequestLinkPreviewExt as _; @@ -110,7 +109,7 @@ pub async fn work() { let Some(m) = q.get_message() else { app.bot() .answer_callback_query(q.id.clone()) - .text(t!("error.inaccessible-message", locale = "en")) + .text("This message is too old or no longer accessible") .show_alert(true) .await?; From 1f7bc98498ecda5d2fa1ec1784b99164f0e2a855 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 14 Mar 2026 18:55:33 +0300 Subject: [PATCH 6/6] remove --- locales/errors.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/locales/errors.yml b/locales/errors.yml index 82a3780b..53c83919 100644 --- a/locales/errors.yml +++ b/locales/errors.yml @@ -40,10 +40,6 @@ error.general: Сообщить о проблеме на GitHub -error.inaccessible-message: - en: |- - This message is too old or no longer accessible - error.unhandled-request: en: |- 😔 Your request was not handled