-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add user settings feature #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7758b0f
5f5d725
cbedb55
8e671fb
0dd0dd4
bf2451c
21416dd
0c35712
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup | ||
| from telegram.ext import CallbackContext | ||
|
|
||
| from core.services.user_settings import ( | ||
| ensure_user_settings, | ||
| get_auto_fetch_in_dm, | ||
| toggle_auto_fetch_in_dm, | ||
| ) | ||
|
|
||
|
|
||
| async def start_command(update: Update, context: CallbackContext) -> None: | ||
| """Handle /start command: greet user and ensure settings row exists.""" | ||
| user_id = update.effective_user.id | ||
| await ensure_user_settings(user_id) | ||
|
|
||
| await update.message.reply_text( | ||
| "Welcome to FastFetchBot!\n\n" | ||
| "Send me a URL in this chat and I'll fetch the content for you.\n\n" | ||
| "Available commands:\n" | ||
| "/settings — Customize bot behavior\n" | ||
| ) | ||
|
|
||
|
|
||
| async def settings_command(update: Update, context: CallbackContext) -> None: | ||
| """Handle /settings command: show current user settings with toggle buttons.""" | ||
| user_id = update.effective_user.id | ||
| await ensure_user_settings(user_id) | ||
| auto_fetch = await get_auto_fetch_in_dm(user_id) | ||
|
|
||
| keyboard = _build_settings_keyboard(auto_fetch) | ||
| await update.message.reply_text( | ||
| text=_build_settings_text(auto_fetch), | ||
| reply_markup=InlineKeyboardMarkup(keyboard), | ||
| ) | ||
|
|
||
|
|
||
| async def settings_callback(update: Update, context: CallbackContext) -> None: | ||
| """Handle settings toggle button presses.""" | ||
| query = update.callback_query | ||
| await query.answer() | ||
|
|
||
| data = query.data | ||
|
|
||
| if data == "settings:close": | ||
| await query.message.delete() | ||
| return | ||
|
|
||
| if data != "settings:toggle_auto_fetch": | ||
| return | ||
|
|
||
| user_id = update.effective_user.id | ||
| new_value = await toggle_auto_fetch_in_dm(user_id) | ||
|
|
||
| keyboard = _build_settings_keyboard(new_value) | ||
| await query.edit_message_text( | ||
| text=_build_settings_text(new_value), | ||
| reply_markup=InlineKeyboardMarkup(keyboard), | ||
| ) | ||
|
|
||
|
|
||
| def _build_settings_keyboard(auto_fetch: bool) -> list[list[InlineKeyboardButton]]: | ||
| status = "ON" if auto_fetch else "OFF" | ||
| return [ | ||
| [ | ||
| InlineKeyboardButton( | ||
| f"Auto-fetch in DM: {status}", | ||
| callback_data="settings:toggle_auto_fetch", | ||
| ) | ||
| ], | ||
| [ | ||
| InlineKeyboardButton("Close", callback_data="settings:close"), | ||
| ], | ||
| ] | ||
|
|
||
|
|
||
| def _build_settings_text(auto_fetch: bool) -> str: | ||
| status = "enabled" if auto_fetch else "disabled" | ||
| return ( | ||
| f"Your Settings\n\n" | ||
| f"Auto-fetch in DM: {status}\n" | ||
| f"When enabled, URLs sent in private chat will be automatically processed.\n" | ||
| f"When disabled, you will see action buttons to choose how to process each URL." | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| from sqlalchemy import select | ||
|
|
||
| from fastfetchbot_shared.database.session import get_session | ||
| from fastfetchbot_shared.database.models.user_setting import UserSetting | ||
|
|
||
| # In-memory cache of user IDs known to have a settings row. | ||
| # Resets on process restart — cheap way to avoid a DB query on every message. | ||
| _known_user_ids: set[int] = set() | ||
|
|
||
|
|
||
| async def ensure_user_settings(user_id: int) -> None: | ||
| """Create a UserSetting row with defaults if one doesn't exist yet.""" | ||
| if user_id in _known_user_ids: | ||
| return | ||
| async with get_session() as session: | ||
| result = await session.execute( | ||
| select(UserSetting).where(UserSetting.telegram_user_id == user_id) | ||
| ) | ||
| if result.scalar_one_or_none() is None: | ||
| session.add(UserSetting(telegram_user_id=user_id)) | ||
| _known_user_ids.add(user_id) | ||
|
|
||
|
|
||
| async def get_auto_fetch_in_dm(user_id: int) -> bool: | ||
| """Return the user's auto_fetch_in_dm preference. Defaults to True.""" | ||
| async with get_session() as session: | ||
| result = await session.execute( | ||
| select(UserSetting.auto_fetch_in_dm).where( | ||
| UserSetting.telegram_user_id == user_id | ||
| ) | ||
| ) | ||
| value = result.scalar_one_or_none() | ||
| return value if value is not None else True | ||
|
|
||
|
|
||
| async def toggle_auto_fetch_in_dm(user_id: int) -> bool: | ||
| """Toggle auto_fetch_in_dm for the given user. Returns the new value.""" | ||
| async with get_session() as session: | ||
| result = await session.execute( | ||
| select(UserSetting).where(UserSetting.telegram_user_id == user_id) | ||
| ) | ||
| user_setting = result.scalar_one_or_none() | ||
| if user_setting is None: | ||
| # Safety fallback — ensure_user_settings should have been called, | ||
| # but handle gracefully. | ||
| user_setting = UserSetting( | ||
| telegram_user_id=user_id, auto_fetch_in_dm=False | ||
| ) | ||
| session.add(user_setting) | ||
| else: | ||
| user_setting.auto_fetch_in_dm = not user_setting.auto_fetch_in_dm | ||
| return user_setting.auto_fetch_in_dm | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible
IntegrityErrorunder concurrent requests for the same new user.If two messages from the same (new) user arrive near-simultaneously, both coroutines can pass the cache check, both query and find no row, and both attempt to insert. The second commit will raise an
IntegrityErroron the primary key. While narrow, this is possible in asyncio due toawaitsuspension points.A simple fix is to catch
IntegrityErrorand treat it as a no-op:🛡️ Proposed fix
🤖 Prompt for AI Agents