Skip to content

feat: Export saved articles to Markdown#5

Open
AminSS99 wants to merge 1 commit intomainfrom
feature-export-articles-5192149412159704075
Open

feat: Export saved articles to Markdown#5
AminSS99 wants to merge 1 commit intomainfrom
feature-export-articles-5192149412159704075

Conversation

@AminSS99
Copy link
Owner

@AminSS99 AminSS99 commented Mar 5, 2026

Implements a feature to export saved articles as a Markdown file.

  1. New command: Added /export in functions/telegram_bot.py.
  2. Implementation: Fetches up to 500 saved articles using get_saved_articles and streams them to the user via Telegram's reply_document inside an io.BytesIO buffer.
  3. Menus: Added /export to the English and Russian menus inside setup_bot_commands.
  4. Translations: Added English and Russian export_empty and export_caption to functions/translations.py.

PR created automatically by Jules for task 5192149412159704075 started by @AminSS99

Added an /export command allowing users to download their saved articles
as a markdown file (.md). The command fetches up to 500 articles and
generates the file in-memory using io.BytesIO.
Updated commands list in English and Russian, and added translation keys.

Co-authored-by: AminSS99 <139346033+AminSS99@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new feature that enables users to export their saved articles from the bot into a Markdown file. This enhancement provides users with a convenient way to retrieve and store their curated content outside the application, improving data portability and user experience by allowing offline access or integration with other tools.

Highlights

  • New /export command: A new /export command has been introduced in functions/telegram_bot.py to allow users to export their saved articles.
  • Markdown Export Implementation: The /export command fetches up to 500 saved articles, formats them into a Markdown file, and streams this file to the user via Telegram's reply_document using an io.BytesIO buffer.
  • Menu Integration: The /export command has been added to both the English and Russian bot command menus within setup_bot_commands for user accessibility.
  • Internationalization Support: New translation keys, export_empty and export_caption, have been added to functions/translations.py to support English and Russian messages related to the export feature.
Changelog
  • functions/telegram_bot.py
    • Added export_command to handle the new /export command, including logic for fetching articles, formatting them into Markdown, and sending the file.
    • Integrated the /export command into the English bot command list.
    • Integrated the /export command into the Russian bot command list.
    • Registered export_command with the bot's application handler.
  • functions/translations.py
    • Added export_empty translation key for English, providing a message when no articles are available for export.
    • Added export_caption translation key for English, providing a caption for the exported document.
    • Added export_empty translation key for Russian, providing a message when no articles are available for export.
    • Added export_caption translation key for Russian, providing a caption for the exported document.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new "/export" command, allowing users to download their saved articles as a Markdown file. However, it contains a Markdown injection vulnerability because user-provided article titles and URLs are embedded into the Markdown file without proper escaping, which could allow for the injection of arbitrary Markdown content. Additionally, the review addresses improving maintainability and correctness by moving hardcoded text to translation files and refactoring a repeated filename into a constant.

# 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'))

Comment on lines +650 to +670
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')
url = article.get('url', '')
category = article.get('category', 'tech')
date_str = article.get('saved_at', '')[:10]

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

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}*"

Comment on lines +677 to +685
# 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))
)

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))
    )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant