init: Initial commit
This commit is contained in:
48
bot.py
48
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 <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
42
commands/test.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
database.db
Normal file
Binary file not shown.
76
devTools/m365agentsplayground.log
Normal file
76
devTools/m365agentsplayground.log
Normal 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
|
||||
22
handler.py
22
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.")
|
||||
|
||||
12
main.py
12
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.")
|
||||
|
||||
19
models/channel.py
Normal file
19
models/channel.py
Normal 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
17
models/service.py
Normal 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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
29
modules/template.py
Normal 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
25
templates/card.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user