diff --git a/src/moai/bot/handlers/__init__.py b/src/moai/bot/handlers/__init__.py index 1709b18..b385aff 100644 --- a/src/moai/bot/handlers/__init__.py +++ b/src/moai/bot/handlers/__init__.py @@ -16,6 +16,7 @@ from moai.bot.handlers.discussion import ( open_command, stop_command, ) +from moai.bot.handlers.export import export_command from moai.bot.handlers.projects import project_command, projects_command from moai.bot.handlers.status import status_command @@ -44,6 +45,7 @@ def register_handlers(app: Application) -> None: app.add_handler(CommandHandler("next", next_command)) app.add_handler(CommandHandler("stop", stop_command)) app.add_handler(CommandHandler("consensus", consensus_command)) + app.add_handler(CommandHandler("export", export_command)) # @mention handler - MessageHandler registered AFTER CommandHandlers # Matches messages starting with @claude, @gpt, or @gemini followed by content diff --git a/src/moai/bot/handlers/commands.py b/src/moai/bot/handlers/commands.py index fbb8a7f..8bea1a3 100644 --- a/src/moai/bot/handlers/commands.py +++ b/src/moai/bot/handlers/commands.py @@ -28,7 +28,8 @@ Multi-AI collaborative brainstorming platform. *Output Commands:* /consensus - Generate consensus summary -/export - Export project as markdown +/export - Export current discussion as markdown +/export project - Export entire project as markdown *Utility:* /status - Show current state diff --git a/src/moai/bot/handlers/export.py b/src/moai/bot/handlers/export.py new file mode 100644 index 0000000..86d0126 --- /dev/null +++ b/src/moai/bot/handlers/export.py @@ -0,0 +1,85 @@ +"""Export command handlers for MoAI bot. + +Provides /export command to export discussions and projects as markdown documents. +""" + +import io + +from telegram import Update +from telegram.ext import ContextTypes + +from moai.core.exporter import export_discussion, export_project +from moai.core.services.discussion import get_active_discussion, get_discussion, list_discussions +from moai.core.services.project import get_project + + +def get_selected_project(context: ContextTypes.DEFAULT_TYPE) -> str | None: + """Get the currently selected project ID from user data.""" + return context.user_data.get("selected_project_id") + + +async def export_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle /export command - export discussion or project as markdown. + + Usage: + /export - Export current active discussion + /export project - Export entire project with all discussions + """ + project_id = get_selected_project(context) + if not project_id: + await update.message.reply_text("No project selected. Use /project select first.") + return + + project = await get_project(project_id) + if not project: + await update.message.reply_text("Selected project not found. Use /project select.") + return + + args = context.args + export_full_project = args and args[0].lower() == "project" + + if export_full_project: + # Export entire project + discussions_list = await list_discussions(project_id) + if not discussions_list: + await update.message.reply_text("No discussions in this project to export.") + return + + # Fetch each discussion with full eager loading + loaded_discussions = [] + for disc in discussions_list: + full_disc = await get_discussion(disc.id) + if full_disc: + loaded_discussions.append(full_disc) + + markdown = export_project(project, loaded_discussions) + filename = f"{project.name.replace(' ', '_')}-export.md" + + await update.message.reply_document( + document=io.BytesIO(markdown.encode("utf-8")), + filename=filename, + caption=f"📄 Full project export: {project.name}", + ) + else: + # Export active discussion + discussion = await get_active_discussion(project_id) + if not discussion: + await update.message.reply_text( + "No active discussion. Start one with /open or /discuss." + ) + return + + # Re-fetch with full loading including consensus + full_discussion = await get_discussion(discussion.id) + if not full_discussion: + await update.message.reply_text("Failed to load discussion.") + return + + markdown = export_discussion(full_discussion) + filename = f"{project.name.replace(' ', '_')}-discussion.md" + + await update.message.reply_document( + document=io.BytesIO(markdown.encode("utf-8")), + filename=filename, + caption=f"📄 Discussion export: {full_discussion.question[:50]}...", + )