diff --git a/.gitignore b/.gitignore index 091964b..d45b969 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .idea/ .venv/ __pycache__/ +devTools/ +*.log .env -CLAUDE.md \ No newline at end of file +CLAUDE.md +database.db \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ab1f416..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 590a59e..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 45f24ab..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/taro-bot-teams.iml b/.idea/taro-bot-teams.iml deleted file mode 100644 index c03f621..0000000 --- a/.idea/taro-bot-teams.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bot.py b/bot.py index 342c611..b685eca 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ from config import DefaultConfig from commands.webhook import WebhookCommand from commands.test import TestCommand from modules.database import DatabaseManager +from modules.command import Command import re @@ -20,11 +21,11 @@ class WebhookBot(ActivityHandler): self.webhook_manager = WebhookCommand(database) self.database = database - def is_command(self, text: str) -> tuple[bool, str | None]: + def is_command(self, text: str) -> tuple[bool, str]: """Check if the text is a command.""" if text.startswith(self.command_prefix) and len(text) > len(self.command_prefix): return True, text[len(self.command_prefix):].split(" ")[0] - return False, None + return False, "" def has_sub_command(self, text: str) -> tuple[bool, str | None]: """Check if the text has a sub command.""" @@ -117,10 +118,8 @@ class WebhookBot(ActivityHandler): # Check if this is a channel conversation is_channel = self.is_channel_conversation(turn_context) - print(f"[Bot] Is channel conversation: {is_channel}") if not is_channel: - print(f"[Bot] Not a channel conversation, ignoring message") return # Get text from activity - can be None for channel posts with only attachments @@ -131,39 +130,13 @@ class WebhookBot(ActivityHandler): # Strip whitespace and check if it's a command text = text.strip() - print(f"[Bot] Processed text (after removing mentions): '{text}'") + message = text.split(" ")[1:] if len(text.split(" ")) > 1 else None + message = " ".join(message) if message is not None else None is_command, command = self.is_command(text) - print(f"[Bot] Is command: {is_command}, Command: {command}") if is_command: - match command: - case "test": - # Get the text behind the command /test - # Remove the command from the text - message = text.replace(f"/test ", "").strip() - if message == "": - await turn_context.send_activity(f"Please provide a message for the test command.") - return - await TestCommand(self.database).handle_test(turn_context, message) - return - case "webhooks": - is_sub_command, sub_command = self.has_sub_command(text) - if is_sub_command: - match sub_command: - case "list": - await self.webhook_manager.handle_list_webhooks(turn_context) - return - case _: - await turn_context.send_activity(f"Sub command {sub_command} not found.") - return - else: - # Default to list if no sub-command specified - await self.webhook_manager.handle_list_webhooks(turn_context) - return - case _: - await turn_context.send_activity(f"Command {command} not found.") - return + cmd = Command(turn_context, self.database) + await cmd.handle_command(command, message) else: - print(f"[Bot] Not a command, echoing message") await turn_context.send_activity(f"Message: {text}") - return \ No newline at end of file + return \ No newline at end of file diff --git a/commands/help.py b/commands/help.py new file mode 100644 index 0000000..a135b2c --- /dev/null +++ b/commands/help.py @@ -0,0 +1,44 @@ +from botbuilder.core import TurnContext, MessageFactory, CardFactory +from botbuilder.schema import Attachment + +from modules.database import DatabaseManager +from modules.template import TemplateEngine + +class HelpCommand: + def __init__(self, turn_context: TurnContext, database: DatabaseManager) -> None: + self.turn_context = turn_context + self.database = database + + async def handle_help(self) -> None: + template = TemplateEngine("help-command-card") + card = template.json_dict({ + "card_title": "Help", + "card_icon": "🚨", + "card_description": "You can use the following commands to interact with the bot:", + }) + commands = [ + { + "title": "**/help**", + "value": "Show this help message" + }, + { + "title": "**/test** ", + "value": "Test the bot" + }, + { + "title": "**/webhooks** create", + "value": "Create a new webhook" + }, + { + "title": "**/webhooks** delete", + "value": "Delete a webhook" + }, + { + "title": "**/webhooks** list", + "value": "List all webhooks" + } + ] + card["body"][2]["facts"] = commands + card_attachment = CardFactory.adaptive_card(card) + await self.turn_context.send_activity(MessageFactory.attachment(card_attachment)) + return \ No newline at end of file diff --git a/commands/test.py b/commands/test.py index d9c3167..1905525 100644 --- a/commands/test.py +++ b/commands/test.py @@ -10,32 +10,28 @@ class TestCommand: def __init__(self, database: DatabaseManager): self.database = database - async def handle_test(self, turn_context: TurnContext, message: str) -> None: + async def handle_test(self, turn_context: TurnContext, command: str, message: str | None = None) -> None: + if message is None: + message = "No message provided" try: activity = turn_context.activity - # conversation_reference = TurnContext.get_conversation_reference(turn_context.activity) - # Check if the channel is a Microsoft Teams channel (msteams) or emulator (for testing) channel_id_str = activity.channel_id - channel_data = activity.channel_data - # channel_data is a dictionary, access it as such - teams_id = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None + microsoft_teams_id_str = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None microsoft_channel_id_str = channel_data.get('teamsChannelId') if channel_data and isinstance(channel_data, dict) else None if channel_id_str == "emulator": - teams_id = "00000000-0000-0000-0000-000000000000" + microsoft_teams_id_str = "00000000-0000-0000-0000-000000000000" microsoft_channel_id_str = "00000000-0000-0000-0000-000000000000" - # Load template and prepare data template = TemplateEngine("card") - # Create and send the Adaptive Card - card_attachment = CardFactory.adaptive_card(template.generate({"card_type": "Test", "card_title": "Test", "card_content": message})) + card_attachment = CardFactory.adaptive_card(template.generate({"card_type": "Test", "card_icon": "🔎", "card_title": "Test", "card_content": f"_{message}_", "card_channel_id": microsoft_teams_id_str, "card_teams_id": microsoft_channel_id_str, "card_debug": json.dumps(channel_data, indent=4)})) - # Send the attachment as a reply to the post in the channel - result = await turn_context.send_activity(MessageFactory.attachment(card_attachment)) + await turn_context.send_activity(MessageFactory.attachment(card_attachment)) + return except Exception as e: import traceback traceback.print_exc() diff --git a/commands/webhook.py b/commands/webhook.py index 634ec9f..be0df92 100644 --- a/commands/webhook.py +++ b/commands/webhook.py @@ -16,29 +16,23 @@ class WebhookCommand: async def handle_list_webhooks(self, turn_context: TurnContext) -> None: try: activity = turn_context.activity - # conversation_reference = TurnContext.get_conversation_reference(turn_context.activity) - # Check if the channel is a Microsoft Teams channel (msteams) or emulator (for testing) channel_id_str = activity.channel_id - channel_data = activity.channel_data - # channel_data is a dictionary, access it as such - teams_id = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None + microsoft_teams_id_str = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None microsoft_channel_id_str = channel_data.get('teamsChannelId') if channel_data and isinstance(channel_data, dict) else None if channel_id_str == "emulator": - teams_id = "00000000-0000-0000-0000-000000000000" + microsoft_teams_id_str = "00000000-0000-0000-0000-000000000000" microsoft_channel_id_str = "00000000-0000-0000-0000-000000000000" - # Get the actual channel UUID from the database channel_uuid = None webhook_count = 0 if microsoft_channel_id_str: try: from uuid import UUID - # Try to get channel by Microsoft Teams channel ID microsoft_channel_id = UUID(microsoft_channel_id_str) if isinstance(microsoft_channel_id_str, str) else microsoft_channel_id_str channel_obj = self.database.get_channel_by_microsoft_channel_id(str(microsoft_channel_id)) if channel_obj: @@ -47,14 +41,10 @@ class WebhookCommand: except (ValueError, TypeError) as e: webhook_count = 0 - # Load template and prepare data template = TemplateEngine("webhook-overview-card" if webhook_count > 0 else "webhook-overview-no-webhooks-card") - - # Create and send the Adaptive Card card_attachment = self.create_webhooks_card(template, {"webhook_count": webhook_count}) - - # Send the attachment as a reply to the post in the channel - result = await turn_context.send_activity(MessageFactory.attachment(card_attachment)) + await turn_context.send_activity(MessageFactory.attachment(card_attachment)) + return except Exception as e: import traceback traceback.print_exc() diff --git a/database.db b/database.db deleted file mode 100644 index 0f5606a..0000000 Binary files a/database.db and /dev/null differ diff --git a/devTools/m365agentsplayground.log b/devTools/m365agentsplayground.log deleted file mode 100644 index 729f3de..0000000 --- a/devTools/m365agentsplayground.log +++ /dev/null @@ -1,76 +0,0 @@ -debug Telemetry: agents-playground-cli/cliStart {"cleanProperties":{"isExec":"false","argv":",-e,http://localhost:3978/api/messages,-c,emulator"}} - -log Listening on 56150 -log Microsoft 365 Agents Playground is being launched for you to debug the app: http://localhost:56150 -debug started web socket client -debug started web socket client -log Waiting for connection of endpoint: http://localhost:3978/api/messages -log waiting for 1 resources: http://localhost:3978/api/messages -log wait-on(76805) complete -log Events recording disabled. -debug Telemetry: agents-playground-server/eventsRecording {"cleanProperties":{"enabled":"false"}} - -debug Telemetry: agents-playground-server/getConfig {"cleanProperties":{"internalConfig":"{\"locale\":\"en-US\",\"localTimezone\":\"Europe/Berlin\",\"channelId\":\"emulator\",\"debugConfig\":{\"eventsRecordingEnabled\":false}}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"40b98e9d-79b0-4d5f-b810-32da23f6b42e","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"team-id","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"message","conversationId":"team-id;messageid=1767856906530","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug stopping web socket client -debug stopping web socket client -debug started web socket client -debug started web socket client -log Waiting for connection of endpoint: http://localhost:3978/api/messages -log waiting for 1 resources: http://localhost:3978/api/messages -log wait-on(76805) complete -log Events recording disabled. -debug Telemetry: agents-playground-server/eventsRecording {"cleanProperties":{"enabled":"false"}} - -debug Telemetry: agents-playground-server/getConfig {"cleanProperties":{"internalConfig":"{\"locale\":\"en-US\",\"localTimezone\":\"Europe/Berlin\",\"channelId\":\"emulator\",\"debugConfig\":{\"eventsRecordingEnabled\":false}}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"40b98e9d-79b0-4d5f-b810-32da23f6b42e","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"team-id","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"message","conversationId":"team-id;messageid=1767857222140","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug stopping web socket client -debug stopping web socket client -debug started web socket client -debug started web socket client -log Waiting for connection of endpoint: http://localhost:3978/api/messages -log waiting for 1 resources: http://localhost:3978/api/messages -log wait-on(76805) complete -log Events recording disabled. -debug Telemetry: agents-playground-server/eventsRecording {"cleanProperties":{"enabled":"false"}} - -debug Telemetry: agents-playground-server/getConfig {"cleanProperties":{"internalConfig":"{\"locale\":\"en-US\",\"localTimezone\":\"Europe/Berlin\",\"channelId\":\"emulator\",\"debugConfig\":{\"eventsRecordingEnabled\":false}}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"40b98e9d-79b0-4d5f-b810-32da23f6b42e","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"team-id","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"message","conversationId":"team-id;messageid=1767857462550","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug stopping web socket client -debug stopping web socket client -debug started web socket client -debug started web socket client -log Waiting for connection of endpoint: http://localhost:3978/api/messages -log waiting for 1 resources: http://localhost:3978/api/messages -log wait-on(76805) complete -log Events recording disabled. -debug Telemetry: agents-playground-server/eventsRecording {"cleanProperties":{"enabled":"false"}} - -debug Telemetry: agents-playground-server/getConfig {"cleanProperties":{"internalConfig":"{\"locale\":\"en-US\",\"localTimezone\":\"Europe/Berlin\",\"channelId\":\"emulator\",\"debugConfig\":{\"eventsRecordingEnabled\":false}}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"40b98e9d-79b0-4d5f-b810-32da23f6b42e","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"conversationUpdate","conversationId":"team-id","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug Telemetry: agents-playground-server/sendActivity {"cleanProperties":{"activityType":"message","conversationId":"team-id;messageid=1767857505932","headers":"{\"x-ms-agents-playground\":\"true\"}"}} - -debug stopping web socket client -debug stopping web socket client diff --git a/modules/command.py b/modules/command.py new file mode 100644 index 0000000..38cb71d --- /dev/null +++ b/modules/command.py @@ -0,0 +1,20 @@ +from botbuilder.core import TurnContext + +from commands.test import TestCommand +from commands.webhook import WebhookCommand +from commands.help import HelpCommand +from modules.database import DatabaseManager + +class Command: + def __init__(self, turn_context: TurnContext, database: DatabaseManager) -> None: + self.turn_context = turn_context + self.database = database + + async def handle_command(self, command: str, message: str | None = None) -> None: + match command: + case "test": + await TestCommand(self.database).handle_test(self.turn_context, command, message) + case "webhooks": + await WebhookCommand(self.database).handle_list_webhooks(self.turn_context) + case _: + await HelpCommand(self.turn_context, self.database).handle_help() \ No newline at end of file diff --git a/modules/database.py b/modules/database.py index 162432e..b37d938 100644 --- a/modules/database.py +++ b/modules/database.py @@ -59,24 +59,19 @@ class DatabaseManager: def get_webhooks(self) -> list[Webhook]: return self.__session.query(Webhook).all() - def count_webhooks(self) -> int: - return len(self.get_webhooks()) - def get_webhook_by_id(self, id: str) -> Webhook: webhook = self.__session.query(Webhook).filter(Webhook.id == id).first() if webhook is None: raise ValueError(f"Webhook with id {id} not found") return webhook - def get_services(self) -> list[Service]: - return self.__session.query(Service).all() - - def get_service_by_id(self, id: str) -> Service: - service = self.__session.query(Service).filter(Service.id == id).first() - if service is None: - raise ValueError(f"Service with id {id} not found") - return service - + def count_webhooks_by_channel_id(self, channel_id: str | UUID) -> int | None: + try: + channel_uuid = UUID(channel_id) if isinstance(channel_id, str) else channel_id + return len(self.__session.query(Webhook).filter(Webhook.channel_id == channel_uuid).all()) + except (ValueError, TypeError): + return None + def get_channels(self) -> list[Channel]: return self.__session.query(Channel).all() @@ -86,22 +81,17 @@ class DatabaseManager: raise ValueError(f"Channel with id {id} not found") return channel - def get_channel_by_microsoft_channel_id(self, microsoft_channel_id: str) -> Channel | None: - """Get channel by Microsoft Teams channel ID. Returns None if not found.""" - try: - # Convert string to UUID if needed - channel_uuid = UUID(microsoft_channel_id) if isinstance(microsoft_channel_id, str) else microsoft_channel_id - channel = self.__session.query(Channel).filter(Channel.microsoft_channel_id == channel_uuid).first() - return channel - except (ValueError, TypeError): - return None + def get_channel_by_microsoft_channel_id(self, microsoft_channel_id: str) -> Channel: + channel = self.__session.query(Channel).filter(Channel.microsoft_channel_id == microsoft_channel_id).first() + if channel is None: + raise ValueError(f"Channel with microsoft channel id {microsoft_channel_id} not found") + return channel - def count_webhooks_by_channel_id(self, channel_id: str | UUID) -> int: - """Count webhooks by channel ID. Accepts UUID string or UUID object.""" - try: - # Convert string to UUID if needed - channel_uuid = UUID(channel_id) if isinstance(channel_id, str) else channel_id - return len(self.__session.query(Webhook).filter(Webhook.channel_id == channel_uuid).all()) - except (ValueError, TypeError): - # If channel_id is not a valid UUID, return 0 - return 0 \ No newline at end of file + def get_services(self) -> list[Service]: + return self.__session.query(Service).all() + + def get_service_by_id(self, id: str) -> Service: + service = self.__session.query(Service).filter(Service.id == id).first() + if service is None: + raise ValueError(f"Service with id {id} not found") + return service \ No newline at end of file diff --git a/modules/template.py b/modules/template.py index a3478dd..3d4c63e 100644 --- a/modules/template.py +++ b/modules/template.py @@ -2,6 +2,7 @@ import os import json from typing import Any +from botbuilder.schema import Attachment class TemplateEngine: def __init__(self, template_name: str, template_path: str = "templates/") -> None: self.__name = template_name @@ -23,7 +24,22 @@ class TemplateEngine: return obj.format(**data) else: return obj - - def generate(self, data: dict) -> dict[str, Any]: + + def __generate_card(self, data: dict) -> dict[str, Any]: template = self.__load_template() - return self.__replace_placeholders(template, data) \ No newline at end of file + return self.__replace_placeholders(template, data) + + def generate(self, data: dict) -> dict[str, Any]: + return self.__generate_card(data) + + def attachment(self, data: dict) -> Attachment: + return Attachment( + content_type="application/vnd.microsoft.card.adaptive", + content=self.generate(data) + ) + + def json_dict(self, data: dict) -> dict: + return self.generate(data) + + def json(self, data: dict) -> str: + return json.dumps(self.generate(data)) \ No newline at end of file diff --git a/templates/card.json b/templates/card.json index eec7c6e..ddcae5e 100644 --- a/templates/card.json +++ b/templates/card.json @@ -14,12 +14,31 @@ "wrap": true, "style": "heading", "size": "ExtraLarge", - "text": "{card_title}" + "text": "{card_icon} {card_title}" }, { "type": "TextBlock", "text": "{card_content}", "wrap": true + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Channel", + "value": "{card_channel_id}" + }, + { + "title": "Teams", + "value": "{card_teams_id}" + } + ] + }, + { + "type": "TextBlock", + "text": "{card_debug}", + "wrap": true, + "fontType": "Monospace" } ] } \ No newline at end of file diff --git a/templates/help-command-card.json b/templates/help-command-card.json new file mode 100644 index 0000000..dfed599 --- /dev/null +++ b/templates/help-command-card.json @@ -0,0 +1,23 @@ +{ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "text": "{card_icon} {card_title}", + "wrap": true, + "style": "heading", + "size": "ExtraLarge" + }, + { + "type": "TextBlock", + "text": "{card_description}", + "wrap": true + }, + { + "type": "FactSet", + "facts": [] + } + ] +} \ No newline at end of file