Skip to content
Merged

Dev #65

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ jobs:
run: uv sync --all-groups

- name: Run Lizard complexity check
run: uv run lizard -l python --CCN 15 -w botspot
run: uv run lizard -l python --CCN 20 -w botspot
2 changes: 1 addition & 1 deletion .github/workflows/push-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: uv sync --all-groups

- name: Run Pyright type checker
run: uv run pyright botspot
run: uv run pyright src
continue-on-error: true

tests:
Expand Down
29 changes: 15 additions & 14 deletions botspot/components/data/access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@

from aiogram.filters import Command
from aiogram.types import Message
from botspot.components.middlewares.i18n import t
from botspot.utils.admin_filter import AdminFilter
from botspot.utils.internal import get_logger
from pydantic_settings import BaseSettings

if TYPE_CHECKING:
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo.asynchronous.collection import AsyncCollection # noqa: vulture

logger = get_logger()

Expand All @@ -39,7 +40,7 @@ class AccessControl:
"""Manages persistent friends and admins lists with MongoDB support."""

def __init__(
self, settings: AccessControlSettings, collection: Optional["AsyncIOMotorCollection"] = None
self, settings: AccessControlSettings, collection: Optional["AsyncCollection"] = None
):
self.settings = settings
self._mongo_available = None
Expand Down Expand Up @@ -276,18 +277,18 @@ async def add_friend_command_handler(message: Message):
)

if username is None:
await message.reply("❌ Failed to get username. Operation cancelled.")
await message.reply(t("access_control.add_friend_failed"))
return

try:
success = await add_friend(username)
if success:
await message.reply(f"✅ Successfully added {username} to friends list!")
await message.reply(t("access_control.add_friend_success", username=username))
else:
await message.reply(f"ℹ️ {username} is already in the friends list.")
await message.reply(t("access_control.add_friend_already_exists", username=username))
except Exception as e:
logger.error(f"Error adding friend {username}: {e}")
await message.reply(f"❌ Error adding friend: {str(e)}")
await message.reply(t("access_control.add_friend_error", error=str(e)))


async def remove_friend_command_handler(message: Message):
Expand All @@ -307,18 +308,18 @@ async def remove_friend_command_handler(message: Message):
)

if username is None:
await message.reply("❌ Failed to get username. Operation cancelled.")
await message.reply(t("access_control.remove_friend_failed"))
return

try:
success = await remove_friend(username)
if success:
await message.reply(f"✅ Successfully removed {username} from friends list!")
await message.reply(t("access_control.remove_friend_success", username=username))
else:
await message.reply(f"ℹ️ {username} was not in the friends list.")
await message.reply(t("access_control.remove_friend_not_found", username=username))
except Exception as e:
logger.error(f"Error removing friend {username}: {e}")
await message.reply(f"❌ Error removing friend: {str(e)}")
await message.reply(t("access_control.remove_friend_error", error=str(e)))


async def list_friends_command_handler(message: Message):
Expand All @@ -329,20 +330,20 @@ async def list_friends_command_handler(message: Message):
friends = await get_friends()

if not friends:
await message.reply("ℹ️ No friends in the list.")
await message.reply(t("access_control.list_friends_empty"))
return

response = "<b>👥 Friends List:</b>\n\n"
response = t("access_control.list_friends_header")
for i, friend in enumerate(friends, 1):
response += f"{i}. {friend}\n"

response += f"\n<i>Total: {len(friends)} friends</i>"
response += t("access_control.list_friends_total", count=len(friends))

await message.reply(response)

except Exception as e:
logger.error(f"Error listing friends: {e}")
await message.reply(f"❌ Error listing friends: {str(e)}")
await message.reply(t("access_control.list_friends_error", error=str(e)))


def setup_dispatcher(dp):
Expand Down
3 changes: 2 additions & 1 deletion botspot/components/data/contact_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
# from botspot.utils.internal import get_logger
#
# if TYPE_CHECKING:
# from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorDatabase # noqa: F401
# from pymongo.asynchronous.collection import AsyncCollection # noqa: F401
# from pymongo.asynchronous.database import AsyncDatabase # noqa: F401
#
# from botspot.core.botspot_settings import BotspotSettings
#
Expand Down
24 changes: 12 additions & 12 deletions botspot/components/data/mongo_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from botspot.utils.internal import get_logger

if TYPE_CHECKING:
from motor.motor_asyncio import AsyncIOMotorClient # noqa: F401
from motor.motor_asyncio import AsyncIOMotorDatabase # noqa: F401
from pymongo import AsyncMongoClient # noqa: F401
from pymongo.asynchronous.database import AsyncDatabase # noqa: F401

logger = get_logger()

Expand All @@ -28,7 +28,7 @@ def setup_dispatcher(dp):
return dp


def get_database() -> "AsyncIOMotorDatabase":
def get_database() -> "AsyncDatabase":
"""Get MongoDB database instance from dependency manager."""
from botspot.core.dependency_manager import get_dependency_manager

Expand All @@ -38,7 +38,7 @@ def get_database() -> "AsyncIOMotorDatabase":
return db


def get_mongo_client() -> "AsyncIOMotorClient":
def get_mongo_client() -> "AsyncMongoClient":
"""Get MongoDB client instance from dependency manager."""
from botspot.core.dependency_manager import get_dependency_manager

Expand All @@ -52,34 +52,34 @@ def get_mongo_client() -> "AsyncIOMotorClient":

def initialize(
settings: MongoDatabaseSettings,
) -> Tuple[Optional["AsyncIOMotorClient"], Optional["AsyncIOMotorDatabase"]]:
) -> Tuple[Optional["AsyncMongoClient"], Optional["AsyncDatabase"]]:
"""Initialize MongoDB connection and return both client and database.

Args:
settings: MongoDB settings

Returns:
Tuple of (AsyncIOMotorClient, AsyncIOMotorDatabase) or (None, None) if disabled
Tuple of (AsyncMongoClient, AsyncDatabase) or (None, None) if disabled

Raises:
ImportError: If motor is not installed
ImportError: If pymongo is not installed
"""
# Check if MongoDB is enabled
if not settings.enabled:
logger.info("MongoDB is disabled")
return None, None

# Check if motor is installed
# Check if pymongo is installed
try:
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo import AsyncMongoClient
except ImportError:
logger.error("motor package is not installed. Please install it to use MongoDB.")
logger.error("pymongo package is not installed. Please install it to use MongoDB.")
raise ImportError(
"motor package is not installed. Run 'poetry add motor' or 'pip install motor'"
"pymongo package is not installed. Run 'uv add pymongo' or 'pip install pymongo'"
)

# Initialize client and database
client = AsyncIOMotorClient(settings.conn_str.get_secret_value())
client = AsyncMongoClient(settings.conn_str.get_secret_value())
db = client[settings.database]
logger.info(f"MongoDB client initialized. Database: {settings.database}")
return client, db
Expand Down
18 changes: 9 additions & 9 deletions botspot/components/data/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from botspot.utils.internal import get_logger

if TYPE_CHECKING:
from motor.motor_asyncio import AsyncIOMotorCollection # noqa: F401
from motor.motor_asyncio import AsyncIOMotorDatabase # noqa: F401
from pymongo.asynchronous.collection import AsyncCollection # noqa: F401
from pymongo.asynchronous.database import AsyncDatabase # noqa: F401

from botspot.core.botspot_settings import BotspotSettings

Expand Down Expand Up @@ -57,7 +57,7 @@ class UserManager(Generic[UserT]):

def __init__(
self,
db: "AsyncIOMotorDatabase",
db: "AsyncDatabase",
collection: str,
user_class: Type[UserT],
settings: Optional["BotspotSettings"] = None,
Expand Down Expand Up @@ -108,7 +108,7 @@ async def has_user(self, user_id: int) -> bool:
return bool(await self.get_user(user_id))

@property
def users_collection(self) -> "AsyncIOMotorCollection":
def users_collection(self) -> "AsyncCollection":
return self.db[self.collection]

async def get_users(self, query: Optional[dict] = None) -> list[UserT]:
Expand Down Expand Up @@ -337,20 +337,20 @@ def initialize(settings: "BotspotSettings", user_class=None) -> UserManager:

Raises:
RuntimeError: If MongoDB is not enabled or initialized
ImportError: If motor package is not installed
ImportError: If pymongo package is not installed
"""
# Check that motor is installed
# Check that pymongo is installed
try:
import motor # noqa: F401
import pymongo # noqa: F401
except ImportError:
from botspot.utils.internal import get_logger

logger = get_logger()
logger.error(
"motor package is not installed. Please install it to use the user_data component."
"pymongo package is not installed. Please install it to use the user_data component."
)
raise ImportError(
"motor package is not installed. Please install it with 'poetry add motor' or 'pip install motor' to use the user_data component."
"pymongo package is not installed. Please install it with 'uv add pymongo' or 'pip install pymongo' to use the user_data component."
)

# Check that MongoDB component is enabled
Expand Down
11 changes: 6 additions & 5 deletions botspot/components/features/multi_forward_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic_settings import BaseSettings

# from dev.draft.easter_eggs.main import get_easter_egg
from botspot.components.middlewares.i18n import t
from botspot.utils.internal import get_logger

logger = get_logger()
Expand Down Expand Up @@ -68,7 +69,7 @@ async def chat_handler(self, message: Message, app: MyApp):
await queue.put(message)
return
# test: send user the message count
await message.answer(f"Received {len(messages)} messages")
await message.answer(t("multi_forward.message_count", count=len(messages)))

text = await self.compose_messages(messages)
if self.send_as_file:
Expand Down Expand Up @@ -144,20 +145,20 @@ async def set_send_as_file(self, message: Message, app: MyApp):
text = self.strip_command(text)
if text.lower() in ["0", "false", "no", "off", "disable"]:
self.send_as_file = False
await message.answer("Send as file disabled")
await message.answer(t("multi_forward.send_as_file_disabled"))
else:
self.send_as_file = True
await message.answer("Send as file enabled")
await message.answer(t("multi_forward.send_as_file_enabled"))


async def enable_send_as_file(self, message: Message, app: MyApp):
self.send_as_file = True
await message.answer("Send as file enabled")
await message.answer(t("multi_forward.send_as_file_enabled"))


async def disable_send_as_file(self, message: Message, app: MyApp):
self.send_as_file = False
await message.answer("Send as file disabled")
await message.answer(t("multi_forward.send_as_file_disabled"))


async def multi_forward_handler():
Expand Down
19 changes: 10 additions & 9 deletions botspot/components/features/user_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pydantic import BaseModel
from pydantic_settings import BaseSettings

from botspot.components.middlewares.i18n import t
from botspot.utils.internal import get_logger

logger = get_logger()
Expand Down Expand Up @@ -146,9 +147,11 @@ async def _ask_user_base(
except asyncio.TimeoutError:
if notify_on_timeout:
if default_choice is not None:
question += f"\n\n⏰ Auto-selected: {default_choice}"
question += "\n\n" + t(
"user_interactions.timeout_auto_selected", choice=default_choice
)
else:
question += "\n\n⏰ No response received within the time limit."
question += "\n\n" + t("user_interactions.timeout")
await sent_message.edit_text(question)
return default_choice # None if no default choice
finally:
Expand Down Expand Up @@ -274,7 +277,7 @@ async def handle_user_input(message: types.Message, state: FSMContext) -> None:
bot: Bot = deps.bot
await bot.send_message(
chat_id,
"Sorry, this response came too late or was for a different question. Please try again.",
t("user_interactions.late_response"),
)
await state.clear()
return
Expand Down Expand Up @@ -396,9 +399,7 @@ async def ask_user_choice_raw(
# If adding hint is requested
displayed_question = question
if add_hint:
displayed_question = (
f"{question}\n\nTip: You can choose an option or type your own response."
)
displayed_question = f"{question}\n\n{t('user_interactions.hint')}"

return await _ask_user_base(
chat_id=chat_id,
Expand Down Expand Up @@ -426,13 +427,13 @@ async def handle_choice_callback(callback_query: types.CallbackQuery, state: FSM

active_request = input_manager.get_active_request(chat_id)
if not active_request or active_request.handler_id != handler_id:
await callback_query.answer("This choice is no longer valid.")
await callback_query.answer(t("user_interactions.choice_invalid"))
return

# Protection against multiple callbacks for the same request
if active_request.response is not None:
# Request already has a response, this is likely a retry
await callback_query.answer("Your choice has already been recorded.")
await callback_query.answer(t("user_interactions.choice_recorded"))
return

choice = callback_query.data[7:]
Expand All @@ -444,7 +445,7 @@ async def handle_choice_callback(callback_query: types.CallbackQuery, state: FSM
assert not isinstance(callback_query.message, InaccessibleMessage)

# Edit the message to remove buttons and show selection
new_text = f"{callback_query.message.text}\n\nSelected: {choice}"
new_text = f"{callback_query.message.text}\n\n{t('user_interactions.selected', choice=choice)}"
try:
await callback_query.message.edit_text(new_text)
except TelegramBadRequest as e:
Expand Down
Loading
Loading