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
44 changes: 44 additions & 0 deletions functions/telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,47 @@ async def saved_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(message, disable_web_page_preview=True, reply_markup=reply_markup)



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

telegram_id = update.effective_user.id
user_lang = get_user_language(telegram_id)
articles = get_saved_articles(telegram_id, limit=500)

if not articles:
await update.message.reply_text(t('export_empty', user_lang), parse_mode='Markdown')
return

# Format articles into Markdown
lines = ["# " + ("Saved Articles" if user_lang == 'en' else "Сохраненные статьи"), ""]
for i, article in enumerate(articles, 1):
title = article.get('title', 'Untitled')

Choose a reason for hiding this comment

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

high

The article title is used directly in a Markdown link on line 670. If a title contains special Markdown characters (e.g., [ or ]), it can break the formatting of the exported file. You should escape the title to ensure the output is always valid.

To fix this, first add from .security_utils import escape_markdown_v1 with the other imports at the top of the function (around line 650), and then apply this suggestion.

        title = escape_markdown_v1(article.get('title', 'Untitled'))

url = article.get('url', '')
category = article.get('category', 'tech')
date_str = article.get('saved_at', '')[:10]

line = f"{i}. [{title}]({url}) - *{category}*"
Comment on lines +650 to +670

Choose a reason for hiding this comment

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

security-medium medium

This section contains a Markdown injection vulnerability because user-provided article titles and URLs are directly embedded into Markdown link syntax without proper escaping. This could allow for the injection of arbitrary Markdown content. To remediate this, escape special Markdown characters in the title and url fields using escape_markdown_v1. Additionally, the Markdown file header is hardcoded for English and Russian; for better internationalization, this string should be moved to translations.py using a key like export_title.

    from .user_storage import get_saved_articles, get_user_language
    from .translations import t
    from .security_utils import escape_markdown_v1
    import io

    telegram_id = update.effective_user.id
    user_lang = get_user_language(telegram_id)
    articles = get_saved_articles(telegram_id, limit=500)

    if not articles:
        await update.message.reply_text(t('export_empty', user_lang), parse_mode='Markdown')
        return

    # Format articles into Markdown
    lines = ["# " + ("Saved Articles" if user_lang == 'en' else "Сохраненные статьи"), ""]
    for i, article in enumerate(articles, 1):
        title = escape_markdown_v1(article.get('title', 'Untitled'))
        url = article.get('url', '')
        category = article.get('category', 'tech')
        date_str = article.get('saved_at', '')[:10]

        line = f"{i}. [{title}]({url}) - *{category}*"

if date_str:
line += f" ({date_str})"
lines.append(line)

markdown_content = "\n".join(lines)

# Create file-like object
file_obj = io.BytesIO(markdown_content.encode('utf-8'))
file_obj.name = "saved_articles.md"

await update.message.reply_document(
document=file_obj,
filename="saved_articles.md",
caption=t('export_caption', user_lang, count=len(articles))
)
Comment on lines +677 to +685

Choose a reason for hiding this comment

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

medium

The filename saved_articles.md is hardcoded in multiple places. It's better to define it as a constant once to avoid repetition and improve maintainability.

    # Create file-like object
    filename = "saved_articles.md"
    file_obj = io.BytesIO(markdown_content.encode('utf-8'))
    file_obj.name = filename

    await update.message.reply_document(
        document=file_obj,
        filename=filename,
        caption=t('export_caption', user_lang, count=len(articles))
    )



async def save_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /save command - save an article."""
from .user_storage import save_article, get_user_language, categorize_article
Expand Down Expand Up @@ -1603,6 +1644,7 @@ async def setup_bot_commands(application: Application):
BotCommand("start", "Start the bot"),
BotCommand("news", "Get AI news digest"),
BotCommand("saved", "View saved articles"),
BotCommand("export", "Export saved articles"),
BotCommand("search", "Search articles"),
BotCommand("semsearch", "Semantic search saved"),
BotCommand("filter", "Filter saved by category"),
Expand All @@ -1625,6 +1667,7 @@ async def setup_bot_commands(application: Application):
BotCommand("start", "Запустить бота"),
BotCommand("news", "Получить дайджест новостей"),
BotCommand("saved", "Сохранённые статьи"),
BotCommand("export", "Экспорт статей"),
BotCommand("search", "Поиск статей"),
BotCommand("semsearch", "Умный поиск"),
BotCommand("filter", "Фильтр по категориям"),
Expand Down Expand Up @@ -1691,6 +1734,7 @@ def create_bot_application() -> Application:
# New feature commands
application.add_handler(CommandHandler("saved", saved_command))
application.add_handler(CommandHandler("save", save_command))
application.add_handler(CommandHandler("export", export_command))
application.add_handler(CommandHandler("clear_saved", clear_saved_command))
application.add_handler(CommandHandler("clear", clear_saved_command)) # Alias for /clear_saved
application.add_handler(CommandHandler("search", search_command))
Expand Down
8 changes: 8 additions & 0 deletions functions/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@
'btn_settings': "⚙️ Settings",
'btn_schedule': "⏰ Schedule",
'btn_help': "❓ Help",

'export_empty': "📂 *No saved articles to export!*\n\nStart saving articles first to use this feature.",
'export_caption': "📁 *Export Successful!*\n\nHere are your {count} saved articles in Markdown format.",
'btn_share': "📤 Share",

},
'ru': {
# Existing keys
Expand Down Expand Up @@ -269,7 +273,11 @@
'btn_settings': "⚙️ Настройки",
'btn_schedule': "⏰ Расписание",
'btn_help': "❓ Помощь",

'export_empty': "📂 *Нет сохранённых статей для экспорта!*\n\nСначала сохраните статьи, чтобы использовать эту функцию.",
'export_caption': "📁 *Экспорт завершен!*\n\nВот ваши сохранённые статьи ({count} шт.) в формате Markdown.",
'btn_share': "📤 Поделиться",

}
}

Expand Down