-
Notifications
You must be signed in to change notification settings - Fork 0
✨ Forge: Implement /export command to download saved articles as Markdown #11
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
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -670,6 +670,89 @@ async def save_command(update: Update, context: ContextTypes.DEFAULT_TYPE): | |
| await update.message.reply_text(t('article_exists', user_lang)) | ||
|
|
||
|
|
||
|
|
||
| async def export_command(update: Update, context: ContextTypes.DEFAULT_TYPE): | ||
| """Handle /export command - export saved articles as markdown.""" | ||
| 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 waiting message | ||
| 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) | ||
|
|
||
| if not articles: | ||
| await update.message.reply_text(t('export_empty', user_lang), parse_mode='Markdown') | ||
| return | ||
|
|
||
| # Group by category | ||
| 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) | ||
|
Comment on lines
+695
to
+700
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block for grouping articles by category can be simplified by using 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')}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This call to |
||
| md_lines.append(f"Total articles: {len(articles)}") | ||
| md_lines.append("") | ||
|
|
||
| cat_emoji = { | ||
| 'ai': '🤖', 'security': '🔒', 'crypto': '💰', 'startups': '🚀', | ||
| 'hardware': '💻', 'software': '📱', 'tech': '🔧' | ||
| } | ||
|
|
||
| for cat, items in sorted(by_category.items()): | ||
| emoji = cat_emoji.get(cat, '🔧') | ||
| cat_label = t(f'cat_{cat}', user_lang) | ||
| md_lines.append(f"## {emoji} {cat_label}") | ||
| md_lines.append("") | ||
|
|
||
| for item in items: | ||
| 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) | ||
|
Comment on lines
+721
to
+737
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Remediation: Escape special Markdown characters in the 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) |
||
| md_lines.append("") | ||
|
|
||
| md_text = chr(10).join(md_lines) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| # Create file-like object | ||
| bio = io.BytesIO(md_text.encode('utf-8')) | ||
| bio.name = f"lensai_export_{datetime.now().strftime('%Y%m%d')}.md" | ||
|
|
||
| # Send document | ||
| try: | ||
| await update.message.reply_document( | ||
| document=bio, | ||
| caption=t('export_caption', user_lang, count=len(articles)), | ||
| parse_mode='Markdown' | ||
| ) | ||
| except Exception as e: | ||
| await update.message.reply_text(f"Error exporting: {e}") | ||
|
Comment on lines
+753
to
+754
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This broad 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.")) |
||
|
|
||
| async def clear_saved_command(update: Update, context: ContextTypes.DEFAULT_TYPE): | ||
| """Handle /clear_saved command.""" | ||
| from .user_storage import clear_saved_articles, get_user_language | ||
|
|
@@ -1693,6 +1776,7 @@ def create_bot_application() -> Application: | |
| application.add_handler(CommandHandler("save", save_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("export", export_command)) | ||
| application.add_handler(CommandHandler("search", search_command)) | ||
| application.add_handler(CommandHandler("language", language_command)) | ||
| application.add_handler(CommandHandler("filter", filter_command)) | ||
|
|
||
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.
The article limit
1000is 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.