Skip to content

✨ Forge: Implement /export command to download saved articles as Markdown#11

Open
AminSS99 wants to merge 1 commit intomainfrom
jules-feature-export-command-11378726837750665501
Open

✨ Forge: Implement /export command to download saved articles as Markdown#11
AminSS99 wants to merge 1 commit intomainfrom
jules-feature-export-command-11378726837750665501

Conversation

@AminSS99
Copy link
Owner

@AminSS99 AminSS99 commented Mar 8, 2026

This PR adds an /export command, which allows users to download all their saved articles grouped by category in a cleanly formatted Markdown (.md) file. It leverages io.BytesIO to securely generate the document in-memory without causing disk side-effects, remaining within the project's boundaries and architectural conventions.

Changes:

  • Implemented export_command in functions/telegram_bot.py.
  • Registered the new command in create_bot_application and setup_bot_commands (for the native Telegram menu).
  • Created relevant localization strings in functions/translations.py for both English and Russian, including updating the /help menu.

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

Implemented a highly useful "/export" command that lets users download
their saved articles as a grouped Markdown document. This provides
better data portability and read-it-later capabilities.

- Added `export_command` to `functions/telegram_bot.py`.
- Registered the `/export` command in `setup_bot_commands`.
- Grouped exported links cleanly by category.
- Updated english and russian translations in `functions/translations.py`.
- Modified help text to describe the new command.

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.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@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. The articles are compiled into a structured Markdown file, categorized for easy readability, and delivered directly to the user. This enhancement provides a convenient way for users to access and manage their saved content outside the bot, improving data portability and user experience.

Highlights

  • New /export command: Implemented a new /export command that allows users to download all their saved articles as a Markdown file, grouped by category.
  • In-memory file generation: The export functionality leverages io.BytesIO to generate the Markdown document in-memory, avoiding disk side-effects and adhering to architectural conventions.
  • Localization support: Added new localization strings for the export feature in both English and Russian, including updates to the /help menu for both languages.
Changelog
  • functions/telegram_bot.py
    • Added the export_command function to handle the new /export command logic.
    • Registered the export_command with the bot's application handlers.
  • functions/translations.py
    • Added new English translation keys for export-related messages (export_preparing, export_empty, export_caption).
    • Updated the English /help message to include the new /export command.
    • Added new Russian translation keys for export-related messages.
    • Updated the Russian /help message to include the new /export command.
Activity
  • PR created automatically by Jules for task 11378726837750665501, initiated by @AminSS99.
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 useful /export command for users to download their saved articles as a Markdown file. However, a critical security concern has been identified regarding the lack of sanitization of user-supplied data (article titles and sources) when generating the Markdown content, which could lead to Markdown injection. Additionally, the review focuses on improving code clarity with Pythonic idioms, enhancing robustness through better exception handling, and increasing maintainability by addressing hardcoded values and ensuring timestamp consistency.

Comment on lines +753 to +754
except Exception as e:
await update.message.reply_text(f"Error exporting: {e}")

Choose a reason for hiding this comment

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

high

This broad except Exception as e: clause leaks internal error details to the user, which is a potential information disclosure risk. It's better to log the specific error for debugging and show a generic, user-friendly error message.

    except Exception as e:
        print(f"Error exporting for user {telegram_id}: {e}")
        await update.message.reply_text(t('ai_error', user_lang, error="Could not generate export file."))

Comment on lines +721 to +737
title = item.get('title', 'Untitled')
url = item.get('url', '')
saved_at = item.get('saved_at', '')[:10]
source = item.get('source', '')

if url.startswith('http'):
line = f"- [{title}]({url})"
else:
line = f"- {title}"

meta = []
if source: meta.append(f"Source: {source}")
if saved_at: meta.append(f"Saved: {saved_at}")

if meta:
line += f" ({', '.join(meta)})"
md_lines.append(line)

Choose a reason for hiding this comment

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

security-medium medium

The /export command constructs a Markdown file using article titles and sources directly from the database without escaping. Since these fields can contain arbitrary text from external news sources or user input, an attacker could inject malicious Markdown syntax. This could lead to link spoofing (phishing) or potentially XSS if the exported file is opened in a vulnerable Markdown viewer.

Remediation: Escape special Markdown characters in the title and source fields before including them in the generated Markdown content. You can use the existing escape_markdown_v1 utility or a similar sanitization function.

            title = item.get('title', 'Untitled')
            url = item.get('url', '')
            saved_at = item.get('saved_at', '')[:10]
            source = item.get('source', '')

            # Escape markdown characters to prevent injection
            from .security_utils import escape_markdown_v1
            safe_title = escape_markdown_v1(title)
            safe_source = escape_markdown_v1(source)

            if url.startswith('http'):
                line = f"- [{safe_title}]({url})"
            else:
                line = f"- {safe_title}"

            meta = []
            if safe_source: meta.append(f"Source: {safe_source}")
            if saved_at: meta.append(f"Saved: {saved_at}")

            if meta:
                line += f" ({', '.join(meta)})"
            md_lines.append(line)

await update.message.reply_text(t('export_preparing', user_lang), parse_mode='Markdown')

# Fetch articles (up to 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.

medium

The article limit 1000 is hardcoded. It would be better to define this as a constant at the module level (e.g., EXPORT_ARTICLE_LIMIT = 1000) for improved maintainability and easier configuration in the future.

Comment on lines +695 to +700
by_category = {}
for article in articles:
cat = article.get('category', 'tech')
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(article)

Choose a reason for hiding this comment

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

medium

This block for grouping articles by category can be simplified by using collections.defaultdict. This makes the code more concise and Pythonic. Remember to add from collections import defaultdict to the function's imports at the top.

    from collections import defaultdict
    by_category = defaultdict(list)
    for article in articles:
        by_category[article.get('category', 'tech')].append(article)

# Build markdown content
md_lines = []
md_lines.append("# LensAI - Saved Articles Export")
md_lines.append(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

Choose a reason for hiding this comment

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

medium

This call to datetime.now() and the one on line 744 could produce different values if the function execution crosses a second or day boundary. It's better to call datetime.now() once at the beginning of the function (e.g., now = datetime.now()) and reuse the now variable for consistent timestamps in both the file content and the filename.

md_lines.append(line)
md_lines.append("")

md_text = chr(10).join(md_lines)

Choose a reason for hiding this comment

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

medium

Using "\n".join() is the more standard and readable Python idiom for joining lines with a newline character.

    md_text = "\n".join(md_lines)

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