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/
.venv/
__pycache__/
devTools/
*.log
.env
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>

43
bot.py
View File

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

View File

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

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]:
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
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

View File

@@ -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
@@ -24,6 +25,21 @@ class TemplateEngine:
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)
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,
"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"
}
]
}

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