init: Initial commit

This commit is contained in:
Björn Benouarets
2026-01-14 08:54:04 +01:00
parent a1eca7baef
commit 44ea07000b
18 changed files with 170 additions and 217 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
.idea/ .idea/
.venv/ .venv/
__pycache__/ __pycache__/
devTools/
*.log
.env .env
CLAUDE.md CLAUDE.md
database.db

10
.idea/.gitignore generated vendored
View File

@@ -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/

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/taro-bot-teams.iml" filepath="$PROJECT_DIR$/.idea/taro-bot-teams.iml" />
</modules>
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

41
bot.py
View File

@@ -8,6 +8,7 @@ from config import DefaultConfig
from commands.webhook import WebhookCommand from commands.webhook import WebhookCommand
from commands.test import TestCommand from commands.test import TestCommand
from modules.database import DatabaseManager from modules.database import DatabaseManager
from modules.command import Command
import re import re
@@ -20,11 +21,11 @@ class WebhookBot(ActivityHandler):
self.webhook_manager = WebhookCommand(database) self.webhook_manager = WebhookCommand(database)
self.database = 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.""" """Check if the text is a command."""
if text.startswith(self.command_prefix) and len(text) > len(self.command_prefix): if text.startswith(self.command_prefix) and len(text) > len(self.command_prefix):
return True, text[len(self.command_prefix):].split(" ")[0] 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]: def has_sub_command(self, text: str) -> tuple[bool, str | None]:
"""Check if the text has a sub command.""" """Check if the text has a sub command."""
@@ -117,10 +118,8 @@ class WebhookBot(ActivityHandler):
# Check if this is a channel conversation # Check if this is a channel conversation
is_channel = self.is_channel_conversation(turn_context) is_channel = self.is_channel_conversation(turn_context)
print(f"[Bot] Is channel conversation: {is_channel}")
if not is_channel: if not is_channel:
print(f"[Bot] Not a channel conversation, ignoring message")
return return
# Get text from activity - can be None for channel posts with only attachments # 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 # Strip whitespace and check if it's a command
text = text.strip() 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) is_command, command = self.is_command(text)
print(f"[Bot] Is command: {is_command}, Command: {command}")
if is_command: if is_command:
match command: cmd = Command(turn_context, self.database)
case "test": await cmd.handle_command(command, message)
# Get the text behind the command /test <message>
# 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: 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
else:
print(f"[Bot] Not a command, echoing message")
await turn_context.send_activity(f"Message: {text}") await turn_context.send_activity(f"Message: {text}")
return return

44
commands/help.py Normal file
View File

@@ -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** <message>",
"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

View File

@@ -10,32 +10,28 @@ class TestCommand:
def __init__(self, database: DatabaseManager): def __init__(self, database: DatabaseManager):
self.database = database 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: try:
activity = turn_context.activity 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_id_str = activity.channel_id
channel_data = activity.channel_data channel_data = activity.channel_data
# channel_data is a dictionary, access it as such microsoft_teams_id_str = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None
teams_id = 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 microsoft_channel_id_str = channel_data.get('teamsChannelId') if channel_data and isinstance(channel_data, dict) else None
if channel_id_str == "emulator": 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" microsoft_channel_id_str = "00000000-0000-0000-0000-000000000000"
# Load template and prepare data
template = TemplateEngine("card") template = TemplateEngine("card")
# Create and send the Adaptive Card 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)}))
card_attachment = CardFactory.adaptive_card(template.generate({"card_type": "Test", "card_title": "Test", "card_content": message}))
# Send the attachment as a reply to the post in the channel await turn_context.send_activity(MessageFactory.attachment(card_attachment))
result = await turn_context.send_activity(MessageFactory.attachment(card_attachment)) return
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()

View File

@@ -16,29 +16,23 @@ class WebhookCommand:
async def handle_list_webhooks(self, turn_context: TurnContext) -> None: async def handle_list_webhooks(self, turn_context: TurnContext) -> None:
try: try:
activity = turn_context.activity 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_id_str = activity.channel_id
channel_data = activity.channel_data channel_data = activity.channel_data
# channel_data is a dictionary, access it as such microsoft_teams_id_str = channel_data.get('teamsTeamId') if channel_data and isinstance(channel_data, dict) else None
teams_id = 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 microsoft_channel_id_str = channel_data.get('teamsChannelId') if channel_data and isinstance(channel_data, dict) else None
if channel_id_str == "emulator": 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" microsoft_channel_id_str = "00000000-0000-0000-0000-000000000000"
# Get the actual channel UUID from the database
channel_uuid = None channel_uuid = None
webhook_count = 0 webhook_count = 0
if microsoft_channel_id_str: if microsoft_channel_id_str:
try: try:
from uuid import UUID 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 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)) channel_obj = self.database.get_channel_by_microsoft_channel_id(str(microsoft_channel_id))
if channel_obj: if channel_obj:
@@ -47,14 +41,10 @@ class WebhookCommand:
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
webhook_count = 0 webhook_count = 0
# Load template and prepare data
template = TemplateEngine("webhook-overview-card" if webhook_count > 0 else "webhook-overview-no-webhooks-card") 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}) card_attachment = self.create_webhooks_card(template, {"webhook_count": webhook_count})
await turn_context.send_activity(MessageFactory.attachment(card_attachment))
# Send the attachment as a reply to the post in the channel return
result = await turn_context.send_activity(MessageFactory.attachment(card_attachment))
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()

Binary file not shown.

View File

@@ -1,76 +0,0 @@
debug Telemetry: agents-playground-cli/cliStart {"cleanProperties":{"isExec":"false","argv":"<REDACTED: user-file-path>,-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

20
modules/command.py Normal file
View File

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

View File

@@ -59,23 +59,18 @@ class DatabaseManager:
def get_webhooks(self) -> list[Webhook]: def get_webhooks(self) -> list[Webhook]:
return self.__session.query(Webhook).all() 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: def get_webhook_by_id(self, id: str) -> Webhook:
webhook = self.__session.query(Webhook).filter(Webhook.id == id).first() webhook = self.__session.query(Webhook).filter(Webhook.id == id).first()
if webhook is None: if webhook is None:
raise ValueError(f"Webhook with id {id} not found") raise ValueError(f"Webhook with id {id} not found")
return webhook return webhook
def get_services(self) -> list[Service]: def count_webhooks_by_channel_id(self, channel_id: str | UUID) -> int | None:
return self.__session.query(Service).all() try:
channel_uuid = UUID(channel_id) if isinstance(channel_id, str) else channel_id
def get_service_by_id(self, id: str) -> Service: return len(self.__session.query(Webhook).filter(Webhook.channel_id == channel_uuid).all())
service = self.__session.query(Service).filter(Service.id == id).first() except (ValueError, TypeError):
if service is None: return None
raise ValueError(f"Service with id {id} not found")
return service
def get_channels(self) -> list[Channel]: def get_channels(self) -> list[Channel]:
return self.__session.query(Channel).all() return self.__session.query(Channel).all()
@@ -86,22 +81,17 @@ class DatabaseManager:
raise ValueError(f"Channel with id {id} not found") raise ValueError(f"Channel with id {id} not found")
return channel return channel
def get_channel_by_microsoft_channel_id(self, microsoft_channel_id: str) -> Channel | None: def get_channel_by_microsoft_channel_id(self, microsoft_channel_id: str) -> Channel:
"""Get channel by Microsoft Teams channel ID. Returns None if not found.""" channel = self.__session.query(Channel).filter(Channel.microsoft_channel_id == microsoft_channel_id).first()
try: if channel is None:
# Convert string to UUID if needed raise ValueError(f"Channel with microsoft channel id {microsoft_channel_id} not found")
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 return channel
except (ValueError, TypeError):
return None
def count_webhooks_by_channel_id(self, channel_id: str | UUID) -> int: def get_services(self) -> list[Service]:
"""Count webhooks by channel ID. Accepts UUID string or UUID object.""" return self.__session.query(Service).all()
try:
# Convert string to UUID if needed def get_service_by_id(self, id: str) -> Service:
channel_uuid = UUID(channel_id) if isinstance(channel_id, str) else channel_id service = self.__session.query(Service).filter(Service.id == id).first()
return len(self.__session.query(Webhook).filter(Webhook.channel_id == channel_uuid).all()) if service is None:
except (ValueError, TypeError): raise ValueError(f"Service with id {id} not found")
# If channel_id is not a valid UUID, return 0 return service
return 0

View File

@@ -2,6 +2,7 @@ import os
import json import json
from typing import Any from typing import Any
from botbuilder.schema import Attachment
class TemplateEngine: class TemplateEngine:
def __init__(self, template_name: str, template_path: str = "templates/") -> None: def __init__(self, template_name: str, template_path: str = "templates/") -> None:
self.__name = template_name self.__name = template_name
@@ -24,6 +25,21 @@ class TemplateEngine:
else: else:
return obj return obj
def generate(self, data: dict) -> dict[str, Any]: def __generate_card(self, data: dict) -> dict[str, Any]:
template = self.__load_template() template = self.__load_template()
return self.__replace_placeholders(template, data) 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))

View File

@@ -14,12 +14,31 @@
"wrap": true, "wrap": true,
"style": "heading", "style": "heading",
"size": "ExtraLarge", "size": "ExtraLarge",
"text": "{card_title}" "text": "{card_icon} {card_title}"
}, },
{ {
"type": "TextBlock", "type": "TextBlock",
"text": "{card_content}", "text": "{card_content}",
"wrap": true "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"
} }
] ]
} }

View File

@@ -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": []
}
]
}