Skip to content
Open
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
91 changes: 91 additions & 0 deletions functions/telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,94 @@ async def clear_saved_command(update: Update, context: ContextTypes.DEFAULT_TYPE
await update.message.reply_text(t('cleared_saved', user_lang))


async def export_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /export command - export all saved articles to a Markdown file."""
from .user_storage import get_saved_articles, get_user_language
from .translations import t
import io
from datetime import datetime

telegram_id = update.effective_user.id
user_lang = get_user_language(telegram_id)

# Send loading message
loading_msg = await update.message.reply_text(t('export_loading', user_lang), parse_mode='Markdown')

try:
# Fetch a large number of saved articles (limit 1000)
articles = get_saved_articles(telegram_id, limit=1000)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove hard 1000-item cap from /export

The /export handler promises to export all saved articles, but this query silently truncates results to 1000 entries. In Firestore mode, save_article does not enforce a maximum saved-article count, so users with more than 1000 saved links will receive incomplete exports without any warning. Consider paginating through all documents (or explicitly communicating a limit) so exports are not silently partial.

Useful? React with 👍 / 👎.


if not articles:
await loading_msg.edit_text(t('export_empty', user_lang), parse_mode='Markdown')
return

# Build the Markdown string
date_str = datetime.now(BAKU_TZ).strftime("%Y-%m-%d")
md_lines = [
f"# LensAI Saved Articles Export",
f"*Generated on {date_str}*",
"",
f"**Total articles:** {len(articles)}",
"---",
""
Comment on lines +707 to +713

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The header of the generated Markdown file contains hardcoded English strings ("LensAI Saved Articles Export", "Generated on", "Total articles:"). Since the bot supports multiple languages, these strings should be translated using the t() function to provide a localized experience in the exported file as well. You would need to add corresponding keys to functions/translations.py.

Suggested change
md_lines = [
f"# LensAI Saved Articles Export",
f"*Generated on {date_str}*",
"",
f"**Total articles:** {len(articles)}",
"---",
""
md_lines = [
f"# {t('export_md_title', user_lang)}",
f"*{t('export_md_generated_on', user_lang)} {date_str}*",
"",
f"**{t('export_md_total', user_lang)}:** {len(articles)}",
"---",
""
]

]

# Category emoji mapping for the export
cat_emoji = {
'ai': '🤖', 'security': '🔒', 'crypto': '💰', 'startups': '🚀',
'hardware': '💻', 'software': '📱', 'tech': '🔧'
}
Comment on lines +717 to +720

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This cat_emoji dictionary is also defined in the saved_command function. To improve maintainability and ensure consistency, you should extract this dictionary into a constant at the module level. This will make it easier to update the emojis in one place for both commands.


for i, article in enumerate(articles, 1):
title = article.get('title', 'Untitled')
url = article.get('url', '')
category = article.get('category', 'tech')
saved_at = article.get('saved_at', '')
source = article.get('source', '')

emoji = cat_emoji.get(category, '🔧')

# Formatting saved_at nicely
date_formatted = ""
if saved_at:
try:
# ISO string parsing
dt = datetime.fromisoformat(saved_at.replace('Z', '+00:00'))
date_formatted = dt.strftime("%Y-%m-%d %H:%M")
except ValueError:
date_formatted = saved_at[:16]

# Title as header
md_lines.append(f"### {i}. {emoji} {title}")
md_lines.append(f"- **URL:** [Link]({url})" if url else "- **URL:** None")
if source:
md_lines.append(f"- **Source:** {source}")
md_lines.append(f"- **Category:** {category.capitalize()}")
Comment on lines +742 to +746

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The title and source variables are used to build a Markdown string without being escaped. If these strings contain characters with special meaning in Markdown (e.g., *, _, [), it could corrupt the formatting of the exported file. You should escape these variables to ensure the generated Markdown is always valid.

            from .security_utils import escape_markdown_v1
            md_lines.append(f"### {i}. {emoji} {escape_markdown_v1(title)}")
            md_lines.append(f"- **URL:** [Link]({url})" if url else "- **URL:** None")
            if source:
                md_lines.append(f"- **Source:** {escape_markdown_v1(source)}")
            md_lines.append(f"- **Category:** {category.capitalize()}")

if date_formatted:
md_lines.append(f"- **Saved at:** {date_formatted}")
md_lines.append("") # Empty line after each article

md_content = "\n".join(md_lines)

# Convert to bytes
file_bytes = md_content.encode('utf-8')
bio = io.BytesIO(file_bytes)
bio.name = f"lensai_export_{date_str}.md"

# Send the file
await update.message.reply_document(
document=bio,
caption=t('export_ready', user_lang)
)

# Delete the loading message
await loading_msg.delete()

except Exception as e:
print(f"Export error: {e}")
error_text = f"❌ Error: {str(e)[:100]}"
Comment on lines +768 to +769

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception is acceptable for resilience, but exposing parts of the raw exception message to the user via str(e)[:100] can leak internal implementation details. It's better to log the full exception with user context for debugging and show a generic, translated error message to the user.

        print(f"Export error for user {update.effective_user.id}: {e}")
        error_text = "❌ An unexpected error occurred during export." # Consider using a translation key

await loading_msg.edit_text(error_text)

async def filter_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /filter command - filter saved articles by category."""
from .user_storage import get_saved_articles, get_user_language
Expand Down Expand Up @@ -1607,6 +1695,7 @@ async def setup_bot_commands(application: Application):
BotCommand("semsearch", "Semantic search saved"),
BotCommand("filter", "Filter saved by category"),
BotCommand("recap", "Weekly saved articles recap"),
BotCommand("export", "Export saved articles"),
BotCommand("status", "View your settings"),
BotCommand("language", "Change language"),
BotCommand("sources", "Toggle news sources"),
Expand All @@ -1629,6 +1718,7 @@ async def setup_bot_commands(application: Application):
BotCommand("semsearch", "Умный поиск"),
BotCommand("filter", "Фильтр по категориям"),
BotCommand("recap", "Еженедельная сводка"),
BotCommand("export", "Экспорт сохраненных статей"),
BotCommand("status", "Настройки"),
BotCommand("language", "Язык"),
BotCommand("sources", "Источники новостей"),
Expand Down Expand Up @@ -1697,6 +1787,7 @@ def create_bot_application() -> Application:
application.add_handler(CommandHandler("language", language_command))
application.add_handler(CommandHandler("filter", filter_command))
application.add_handler(CommandHandler("recap", recap_command))
application.add_handler(CommandHandler("export", export_command))
application.add_handler(CommandHandler("share", share_command))
application.add_handler(CommandHandler("trends", trends_command))
application.add_handler(CommandHandler("timezone", timezone_command))
Expand Down
8 changes: 8 additions & 0 deletions functions/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
• /saved - View all saved articles
• /filter <category> - Filter by category (ai, security, crypto, startups, hardware, software, tech)
• /recap - Weekly recap of saved articles
• /export - Export saved articles to Markdown
• /clear_saved - Clear all saved articles

⚙️ **Settings**
Expand Down Expand Up @@ -117,6 +118,9 @@
'recap_empty': "📊 **Weekly Recap**\n\nNo articles saved this week. Start saving articles to see your recap!",
'article_deleted': "🗑️ Article deleted!",
'article_saved_single': "✅ Article saved! Category: {category}",
'export_loading': "⏳ Preparing your export...",
'export_ready': "📄 Here is your exported articles document!",
'export_empty': "📂 You have no saved articles to export.",

# Category labels
'cat_ai': "🤖 AI",
Expand Down Expand Up @@ -190,6 +194,7 @@
• /saved - Просмотреть все сохранённые
• /filter <категория> - Фильтр по категории (ai, security, crypto, startups, hardware, software, tech)
• /recap - Недельный обзор сохранённых
• /export - Экспортировать сохранённые статьи
• /clear_saved - Очистить все сохранённые

⚙️ **Настройки**
Expand Down Expand Up @@ -250,6 +255,9 @@
'recap_empty': "📊 **Недельный обзор**\n\nНет сохранённых статей за неделю. Начните сохранять!",
'article_deleted': "🗑️ Статья удалена!",
'article_saved_single': "✅ Статья сохранена! Категория: {category}",
'export_loading': "⏳ Подготавливаю экспорт...",
'export_ready': "📄 Вот ваш документ с сохранёнными статьями!",
'export_empty': "📂 У вас нет сохранённых статей для экспорта.",

# Category labels
'cat_ai': "🤖 ИИ",
Expand Down