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 0000000..0f5606a
Binary files /dev/null and b/database.db differ
diff --git a/devTools/m365agentsplayground.log b/devTools/m365agentsplayground.log
new file mode 100644
index 0000000..729f3de
--- /dev/null
+++ b/devTools/m365agentsplayground.log
@@ -0,0 +1,76 @@
+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/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",