From a1eca7baefd36b26e4a2cd72ec87eb107a7998ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Thu, 8 Jan 2026 10:10:12 +0100 Subject: [PATCH] init: Initial commit --- bot.py | 48 ++++++----- commands/test.py | 42 ++++++++++ commands/webhook.py | 77 +++++++++--------- config.py | 3 +- database.db | Bin 0 -> 49152 bytes devTools/m365agentsplayground.log | 76 +++++++++++++++++ handler.py | 22 +++-- main.py | 12 +-- models/channel.py | 19 +++++ models/service.py | 17 ++++ models/team.py | 13 +-- models/webhook.py | 16 ++-- modules/database.py | 73 +++++++++++++++-- modules/template.py | 29 +++++++ templates/card.json | 25 ++++++ templates/webhook-overview-card.json | 14 ++-- .../webhook-overview-no-webhooks-card.json | 17 ++-- 17 files changed, 392 insertions(+), 111 deletions(-) create mode 100644 commands/test.py create mode 100644 database.db create mode 100644 devTools/m365agentsplayground.log create mode 100644 models/channel.py create mode 100644 models/service.py create mode 100644 modules/template.py create mode 100644 templates/card.json diff --git a/bot.py b/bot.py index 8895471..342c611 100644 --- a/bot.py +++ b/bot.py @@ -1,11 +1,12 @@ from typing import Tuple from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.schema import ChannelAccount, Mention +from botbuilder.schema import ChannelAccount, Mention, Activity, ActivityTypes from config import DefaultConfig -from commands.webhook import WebhookManager +from commands.webhook import WebhookCommand +from commands.test import TestCommand from modules.database import DatabaseManager import re @@ -16,7 +17,7 @@ class WebhookBot(ActivityHandler): def __init__(self, config: DefaultConfig, database: DatabaseManager) -> None: super().__init__() self.command_prefix = DefaultConfig.COMMAND_PREFIX - self.webhook_manager = WebhookManager(database) + self.webhook_manager = WebhookCommand(database) self.database = database def is_command(self, text: str) -> tuple[bool, str | None]: @@ -44,9 +45,7 @@ class WebhookBot(ActivityHandler): # Check entities for mentions if activity.entities is None: return False - - print(f"[Bot] Checking mentions, bot_id: {bot_id}, entities: {len(activity.entities)}") - + for entity in activity.entities: if entity.type == "mention": try: @@ -54,7 +53,6 @@ class WebhookBot(ActivityHandler): if hasattr(entity, 'mentioned') and entity.mentioned: mentioned_id = getattr(entity.mentioned, 'id', None) if mentioned_id == bot_id: - print(f"[Bot] Bot mentioned (via mentioned property)") return True # Check properties dict (alternative format) @@ -63,14 +61,10 @@ class WebhookBot(ActivityHandler): if isinstance(props, dict): mentioned = props.get('mentioned', {}) if isinstance(mentioned, dict): - if mentioned.get('id') == bot_id: - print(f"[Bot] Bot mentioned (via properties dict)") + if mentioned.get('id') == bot_id: # type: ignore return True except Exception as e: - print(f"[Bot] Error checking mention entity: {e}") continue - - print(f"[Bot] Bot not mentioned") return False def is_channel_conversation(self, turn_context: TurnContext) -> bool: @@ -86,14 +80,22 @@ class WebhookBot(ActivityHandler): # Remove all between and return re.sub(r'.*?', '', text) + def send_message(self, channel_id: str, message: str) -> None: + channel_account = ChannelAccount(id=channel_id) + activity = Activity( + type=ActivityTypes.message, + text=message, + channel_id=channel_id, + from_property=channel_account + ) + + async def on_command(self, command: str, turn_context: TurnContext) -> None: """Handle the command.""" await turn_context.send_activity(f"Command {command} received.") return - async def on_members_added_activity( - self, members_added: list[ChannelAccount], turn_context: TurnContext - ) -> None: + async def on_members_added_activity(self, members_added: list[ChannelAccount], turn_context: TurnContext) -> None: """Handle the members added activity.""" if turn_context.activity.recipient is None: return @@ -112,13 +114,6 @@ class WebhookBot(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext) -> None: """Handle the message activity - works for both 1:1 and channel posts.""" activity = turn_context.activity - print(f"[Bot] on_message_activity called") - print(f"[Bot] Activity type: {activity.type}") - print(f"[Bot] Channel ID: {activity.channel_id}") - print(f"[Bot] Activity text: {activity.text}") - print(f"[Bot] Conversation type: {activity.conversation.conversation_type if activity.conversation else 'None'}") - print(f"[Bot] From: {activity.from_property.name if activity.from_property else 'None'}") - print(f"[Bot] Entities: {activity.entities}") # Check if this is a channel conversation is_channel = self.is_channel_conversation(turn_context) @@ -142,6 +137,15 @@ class WebhookBot(ActivityHandler): 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: diff --git a/commands/test.py b/commands/test.py new file mode 100644 index 0000000..d9c3167 --- /dev/null +++ b/commands/test.py @@ -0,0 +1,42 @@ +from modules.database import DatabaseManager +from botbuilder.core import TurnContext, CardFactory, MessageFactory +from botbuilder.schema import Attachment, Activity, ActivityTypes, ConversationParameters +from aiohttp.web import Response, json_response +import json + +from modules.template import TemplateEngine + +class TestCommand: + def __init__(self, database: DatabaseManager): + self.database = database + + async def handle_test(self, turn_context: TurnContext, message: str) -> 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_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_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})) + + # Send the attachment as a reply to the post in the channel + result = await turn_context.send_activity(MessageFactory.attachment(card_attachment)) + except Exception as e: + import traceback + traceback.print_exc() + raise diff --git a/commands/webhook.py b/commands/webhook.py index 38e297e..634ec9f 100644 --- a/commands/webhook.py +++ b/commands/webhook.py @@ -1,62 +1,61 @@ from modules.database import DatabaseManager from botbuilder.core import TurnContext, CardFactory, MessageFactory -from botbuilder.schema import Attachment, Activity, ActivityTypes +from botbuilder.schema import Attachment, Activity, ActivityTypes, ConversationParameters from aiohttp.web import Response, json_response import json -from typing import cast, Any -class WebhookManager: +from modules.template import TemplateEngine + +class WebhookCommand: def __init__(self, database: DatabaseManager): self.database = database - def __load_template(self, template_name: str) -> dict: - with open(f"templates/{template_name}.json", "r", encoding="utf-8") as f: - return json.loads(f.read()) - - def _replace_placeholders(self, obj, data: dict): - """Recursively replace placeholders in dict/list/string values.""" - if isinstance(obj, dict): - return {k: self._replace_placeholders(v, data) for k, v in obj.items()} - elif isinstance(obj, list): - return [self._replace_placeholders(item, data) for item in obj] - elif isinstance(obj, str): - return obj.format(**data) - else: - return obj - - def _create_webhooks_card(self, template: dict, data: dict) -> Attachment: - # Replace placeholders in dict structure - filled_template = self._replace_placeholders(template, data) - print(f"Filled template: {filled_template}") - # Type cast: filled_template is guaranteed to be a dict since template is a dict - return CardFactory.adaptive_card( - cast(dict[str, Any], filled_template) - ) + def create_webhooks_card(self, template: TemplateEngine, data: dict) -> Attachment: + return CardFactory.adaptive_card(template.generate(data)) async def handle_list_webhooks(self, turn_context: TurnContext) -> None: try: activity = turn_context.activity - channel_id = activity.channel_id + # conversation_reference = TurnContext.get_conversation_reference(turn_context.activity) # Check if the channel is a Microsoft Teams channel (msteams) or emulator (for testing) - if channel_id not in ["msteams", "emulator"]: - await turn_context.send_activity(f"Dieser Befehl ist nur in Microsoft Teams verfügbar.") - return - - webhooks = self.database.get_webhooks() + 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_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_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: + channel_uuid = str(channel_obj.id) + webhook_count = self.database.count_webhooks_by_channel_id(channel_uuid) + except (ValueError, TypeError) as e: + webhook_count = 0 + # Load template and prepare data - template = self.__load_template("webhook-overview-card" if webhooks else "webhook-overview-no-webhooks-card") - data = {"webhook_count": len(webhooks)} + 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, data) - - # Send the attachment directly - send_activity will create the Activity with proper fields + 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)) - print(f"Activity sent successfully, result: {result}") except Exception as e: - print(f"Error in handle_list_webhooks: {e}") import traceback traceback.print_exc() raise diff --git a/config.py b/config.py index 54b61c3..cff37a0 100644 --- a/config.py +++ b/config.py @@ -16,4 +16,5 @@ class DefaultConfig: DB_USER = get_env("DB_USER", "/") DB_PASSWORD = get_env("DB_PASSWORD", "/") DB_HOST = get_env("DB_HOST", "/") - DB_PORT = get_env("DB_PORT", "/") \ No newline at end of file + DB_PORT = get_env("DB_PORT", "/") + DB_PATH = get_env("DB_PATH", "./database.db") \ No newline at end of file diff --git a/database.db b/database.db new file mode 100644 index 0000000000000000000000000000000000000000..0f5606a547da8102cc9d60f6e2ab40327249246c GIT binary patch literal 49152 zcmeI&(N5D)7{KvX#>To0xa$?kNxWGT;xfE8ag3siW0SHOxiC%HN@!#&ZbiKXHSxwb z@PhajJ_Ik~6OedgTXxWCnM4yW>c2^Qy3>C2oZt6tqv>|Gn_l3GeSb7?0&!PaR#a8F zFNC5fnQ$$Jt6ZYj(JB{J?#yU2%AF4vrKJm{pq(j;=S$~HpGsdAKNg>rK9}BWXT{aR z*BQ|qLI42-5I_I{1Q7V|1WupkbNb4P`fe_8_72@w?%sj#KOf6ht6OHRV~S2~y=e;B zE6Q5l>x*u;u_0RRj%an8%~hf0$8K-r2I5K0sz0n*KOGvq^m?wH6xKU%hC}x-`Pj&H z0=I96|2%Jm1L-uj%;-wTNByMk$5vyjX6=ec=B_AveIq)x+iGlgO;NraT->$Uw#>$Z zR&-j}EeB=@%iJ_AvsE{D#H4$C^=E#J`99#Om`V3#jAm2{S-rlZD&DZ~z8=3g4DSPM z=Q!}=*S55@*W_NYqTMK*kZk9tk|BFM,-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/handler.py b/handler.py index 2dad9cf..5026b60 100644 --- a/handler.py +++ b/handler.py @@ -15,29 +15,27 @@ class BotHandler: self.bot = bot async def messages(self, req: Request) -> Response: - try: - # Log incoming request - print(f"[Handler] Received request at /api/messages") - print(f"[Handler] Request method: {req.method}") - print(f"[Handler] Content-Type: {req.headers.get('Content-Type', 'None')}") - print(f"[Handler] Content-Length: {req.headers.get('Content-Length', 'None')}") - + try: # Process the request - adapter will read the body - print(f"[Handler] Processing request with adapter...") response = await self.adapter.process(req, self.bot) if response is not None: - print(f"[Handler] Adapter returned response: {response.status}") return response - print(f"[Handler] Adapter returned None, sending 204") return json_response(status=204) except Exception as e: - print(f"[Handler] Error processing request: {e}") import traceback traceback.print_exc() return json_response(status=500) + async def api_test(self, req: Request) -> Response: + try: + body = await req.json() + # Send a message with the bot to the channel + self.bot.send_message(body.get("channel_id", "00000000-0000-0000-0000-000000000000"), body.get("message", "Hello, world!")) + return json_response(status=200, data={"message": "Message sent successfully"}) + except Exception as e: + return json_response(status=500, data={"message": "Error sending message"}) + async def on_error(self, context: TurnContext, error: Exception) -> None: - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) traceback.print_exc() await context.send_activity("The bot encountered an error or bug.") diff --git a/main.py b/main.py index 7925236..26c2b83 100644 --- a/main.py +++ b/main.py @@ -8,26 +8,20 @@ from bot import WebhookBot from handler import BotHandler from config import DefaultConfig -from modules.database import DatabaseManager +from modules.database import DatabaseManager, SQLiteConnectionString from dotenv import load_dotenv load_dotenv() CONFIG = DefaultConfig() -DATABASE = DatabaseManager( - db_name=CONFIG.DB_NAME, - db_host=CONFIG.DB_HOST, - db_port=CONFIG.DB_PORT, - db_user=CONFIG.DB_USER, - db_password=CONFIG.DB_PASSWORD -) +connection_string = SQLiteConnectionString(CONFIG.DB_PATH) +DATABASE = DatabaseManager(connection_string) ADAPTER = CloudAdapter(ConfigurationBotFrameworkAuthentication(CONFIG)) # Register error handler async def on_error(context, error): - print(f"[Adapter] Error occurred: {error}") import traceback traceback.print_exc() await context.send_activity("An error occurred processing your request.") diff --git a/models/channel.py b/models/channel.py new file mode 100644 index 0000000..21ec234 --- /dev/null +++ b/models/channel.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, String, DateTime, UUID, ForeignKey +from sqlalchemy.orm import relationship +from uuid import uuid4 + +from datetime import datetime + +from models.base import Base + +class Channel(Base): + __tablename__ = "channels" + id = Column(UUID, primary_key=True, default=uuid4) + name = Column(String) + microsoft_channel_id = Column(UUID, nullable=False, unique=True) + team_id = Column(UUID, ForeignKey("teams.id"), nullable=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now) + + team = relationship("Team", back_populates="channels") + webhooks = relationship("Webhook", back_populates="channel", cascade="all, delete-orphan") \ No newline at end of file diff --git a/models/service.py b/models/service.py new file mode 100644 index 0000000..1fdaa68 --- /dev/null +++ b/models/service.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, String, DateTime, UUID, ForeignKey, Boolean +from sqlalchemy.orm import relationship +from uuid import uuid4 + +from datetime import datetime + +from models.base import Base + +class Service(Base): + __tablename__ = "services" + id = Column(UUID, primary_key=True, default=uuid4) + name = Column(String) + important = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now) + + webhooks = relationship("Webhook", back_populates="service", cascade="all, delete-orphan") \ No newline at end of file diff --git a/models/team.py b/models/team.py index 05391bd..7415b8a 100644 --- a/models/team.py +++ b/models/team.py @@ -1,14 +1,17 @@ -from sqlalchemy import Column, String, DateTime, UUID +from sqlalchemy import Column, String, DateTime, UUID from sqlalchemy.orm import relationship from uuid import uuid4 +from datetime import datetime + from models.base import Base class Team(Base): __tablename__ = "teams" id = Column(UUID, primary_key=True, default=uuid4) name = Column(String) - microsoft_team_id = Column(UUID) - created_at = Column(DateTime) - updated_at = Column(DateTime) - webhooks = relationship("Webhook", back_populates="team", cascade="all, delete-orphan") \ No newline at end of file + microsoft_team_id = Column(UUID, nullable=False, unique=True) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now) + + channels = relationship("Channel", back_populates="team", cascade="all, delete-orphan") \ No newline at end of file diff --git a/models/webhook.py b/models/webhook.py index a5937af..2133087 100644 --- a/models/webhook.py +++ b/models/webhook.py @@ -2,14 +2,18 @@ from sqlalchemy import Column, String, DateTime, UUID, ForeignKey from sqlalchemy.orm import relationship from uuid import uuid4 +from datetime import datetime + from models.base import Base class Webhook(Base): __tablename__ = "webhooks" id = Column(UUID, primary_key=True, default=uuid4) - url = Column(String) - secret = Column(String) - team_id = Column(UUID, ForeignKey("teams.id"), nullable=False) - created_at = Column(DateTime) - updated_at = Column(DateTime) - team = relationship("Team", back_populates="webhooks") \ No newline at end of file + secret = Column(String, nullable=False, unique=True) + service_id = Column(UUID, ForeignKey("services.id"), nullable=False) + channel_id = Column(UUID, ForeignKey("channels.id"), nullable=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now) + + channel = relationship("Channel", back_populates="webhooks") + service = relationship("Service", back_populates="webhooks") \ No newline at end of file diff --git a/modules/database.py b/modules/database.py index 28b526f..162432e 100644 --- a/modules/database.py +++ b/modules/database.py @@ -1,16 +1,38 @@ from models.team import Team from models.webhook import Webhook +from models.service import Service +from models.channel import Channel from models.base import Base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.engine import URL from urllib.parse import quote_plus +from uuid import UUID + +class DatabaseConnectionString: + def __init__(self, db_name: str, db_user: str, db_password: str, db_host: str, db_port: str): + self.connection_string = f"postgresql://{db_user}:{quote_plus(db_password)}@{db_host}:{db_port}/{db_name}" + + def __str__(self) -> str: + return self.connection_string + +class PostgresConnectionString(DatabaseConnectionString): + def __init__(self, db_name: str, db_user: str, db_password: str, db_host: str, db_port: str): + super().__init__(db_name, db_user, db_password, db_host, db_port) + +class MySQLConnectionString(DatabaseConnectionString): + def __init__(self, db_name: str, db_user: str, db_password: str, db_host: str, db_port: str): + super().__init__(db_name, db_user, db_password, db_host, db_port) + +class SQLiteConnectionString(DatabaseConnectionString): + def __init__(self, db_path: str): + super().__init__("", "", "", "", "") + self.connection_string = f"sqlite:///{db_path}" class DatabaseManager: - def __init__(self, db_name: str, db_user: str, db_password: str, db_host: str, db_port: str): - # URL encode the password to handle special characters - encoded_password = quote_plus(db_password) - self.engine = create_engine(f"postgresql://{db_user}:{encoded_password}@{db_host}:{db_port}/{db_name}") + def __init__(self, connection_string: DatabaseConnectionString): + self.engine = create_engine(str(connection_string)) self.session = sessionmaker(bind=self.engine) self.__session = self.session() @@ -37,8 +59,49 @@ 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 \ No newline at end of file + 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 get_channels(self) -> list[Channel]: + return self.__session.query(Channel).all() + + def get_channel_by_id(self, id: str) -> Channel: + channel = self.__session.query(Channel).filter(Channel.id == id).first() + if channel is None: + 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 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 diff --git a/modules/template.py b/modules/template.py new file mode 100644 index 0000000..a3478dd --- /dev/null +++ b/modules/template.py @@ -0,0 +1,29 @@ +import os +import json +from typing import Any + +class TemplateEngine: + def __init__(self, template_name: str, template_path: str = "templates/") -> None: + self.__name = template_name + self.__path = template_path + self.__template_path = f"{self.__path}{self.__name}.json" + self.__template = self.__load_template() + + def __load_template(self) -> dict[str, Any]: + with open(self.__template_path, "r", encoding="utf-8") as f: + return json.loads(f.read()) + + def __replace_placeholders(self, obj: Any, data: dict) -> Any: + """Recursively replace placeholders in dict/list/string values.""" + if isinstance(obj, dict): + return {k: self.__replace_placeholders(v, data) for k, v in obj.items()} + elif isinstance(obj, list): + return [self.__replace_placeholders(item, data) for item in obj] + elif isinstance(obj, str): + return obj.format(**data) + else: + return obj + + def generate(self, data: dict) -> dict[str, Any]: + template = self.__load_template() + return self.__replace_placeholders(template, data) \ No newline at end of file diff --git a/templates/card.json b/templates/card.json new file mode 100644 index 0000000..eec7c6e --- /dev/null +++ b/templates/card.json @@ -0,0 +1,25 @@ +{ + "type": "AdaptiveCard", + "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "body": [ + { + "type": "Badge", + "text": "{card_type}", + "size": "Large", + "style": "Accent" + }, + { + "type": "TextBlock", + "wrap": true, + "style": "heading", + "size": "ExtraLarge", + "text": "{card_title}" + }, + { + "type": "TextBlock", + "text": "{card_content}", + "wrap": true + } + ] +} \ No newline at end of file diff --git a/templates/webhook-overview-card.json b/templates/webhook-overview-card.json index 0fa4a89..6b84eb2 100644 --- a/templates/webhook-overview-card.json +++ b/templates/webhook-overview-card.json @@ -3,23 +3,27 @@ "body": [ { "type": "TextBlock", - "text": "Webhooks", + "text": "Webhooks Overview", "size": "Large", "weight": "Bolder", "color": "Accent" }, { "type": "TextBlock", - "text": "Es sind {webhook_count} Webhooks registriert", + "text": "Currently {webhook_count} webhooks are registered.", "size": "Small", "color": "Good", "isSubtle": true, "wrap": true }, { - "type": "TextBlock", - "text": "", - "separator": true + "type": "FactSet", + "facts": [ + { + "title": "Webhook Count", + "value": "{webhook_count}" + } + ] } ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", diff --git a/templates/webhook-overview-no-webhooks-card.json b/templates/webhook-overview-no-webhooks-card.json index 18fb9f8..a63a5d2 100644 --- a/templates/webhook-overview-no-webhooks-card.json +++ b/templates/webhook-overview-no-webhooks-card.json @@ -3,23 +3,26 @@ "body": [ { "type": "TextBlock", - "text": "Webhooks", + "text": "Webhooks Overview", "size": "Large", "weight": "Bolder", "color": "Accent" }, { "type": "TextBlock", - "text": "Es sind keine Webhooks registriert", + "text": "Currently no webhooks for this channel are registered.", "size": "Small", "color": "Good", - "isSubtle": true, - "wrap": true + "isSubtle": true }, { - "type": "TextBlock", - "text": "", - "separator": true + "type": "FactSet", + "facts": [ + { + "title": "Webhook Count", + "value": "0" + } + ] } ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",