init: Initial commit

This commit is contained in:
Björn Benouarets
2026-01-08 10:10:12 +01:00
parent 7055272793
commit a1eca7baef
17 changed files with 392 additions and 111 deletions

48
bot.py
View File

@@ -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 <at> and </at>
return re.sub(r'<at>.*?</at>', '', 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 <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:

42
commands/test.py Normal file
View File

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

View File

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

View File

@@ -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", "/")
DB_PORT = get_env("DB_PORT", "/")
DB_PATH = get_env("DB_PATH", "./database.db")

BIN
database.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,76 @@
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

View File

@@ -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.")

12
main.py
View File

@@ -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.")

19
models/channel.py Normal file
View File

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

17
models/service.py Normal file
View File

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

View File

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

View File

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

View File

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

29
modules/template.py Normal file
View File

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

25
templates/card.json Normal file
View File

@@ -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
}
]
}

View File

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

View File

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