Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions elementary/messages/messaging_integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Elementary Messaging Integration System

## Overview

The Elementary Messaging Integration system provides a flexible and extensible framework for sending alerts and messages to various messaging platforms (e.g., Slack, Teams). The system is designed to support a gradual migration from the legacy integration system to a more generic messaging-based approach.

## Architecture

### BaseMessagingIntegration

The core of the new messaging system is the `BaseMessagingIntegration` abstract class. This class defines the contract that all messaging integrations must follow:

- `send_message()`: Send a message to a specific destination
- `supports_reply()`: Check if the integration supports message threading/replies
- `reply_to_message()`: Reply to an existing message (if supported)

### Key Components

1. **MessageBody**: A platform-agnostic representation of a message
2. **MessageSendResult**: Contains information about a sent message, including timestamp and platform-specific context
3. **DestinationType**: Generic type representing the destination for a message (e.g., webhook URL, channel)
4. **MessageContextType**: Generic type for platform-specific message context

## Migration Strategy

The system currently supports both:

- Legacy `BaseIntegration` implementations (e.g., Slack)
- New `BaseMessagingIntegration` implementations (e.g., Teams)

This dual support allows for a gradual migration path where:

1. New integrations are implemented using `BaseMessagingIntegration`
2. Existing integrations can be migrated one at a time
3. The legacy `BaseIntegration` will eventually be deprecated

## Implementing a New Integration

To add a new messaging platform integration:

1. Create a new class that extends `BaseMessagingIntegration`
2. Implement the required abstract methods:
```python
def send_message(self, destination: DestinationType, body: MessageBody) -> MessageSendResult
def supports_reply(self) -> bool
def reply_to_message(self, destination, message_context, message_body) -> MessageSendResult # if supported
```
3. Update the `Integrations` factory class to support the new integration

## Current Implementations

- **Teams**: Uses the new `BaseMessagingIntegration` system with webhook support
- **Slack**: Currently uses the legacy `BaseIntegration` system (planned for migration)

## Future Improvements

1. Complete migration of Slack to `BaseMessagingIntegration`
2. Add support for more messaging platforms
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Generic, Optional, TypeVar

from pydantic import BaseModel

from elementary.messages.message_body import MessageBody
from elementary.messages.messaging_integrations.exceptions import (
MessageIntegrationReplyNotSupportedError,
)
from elementary.utils.log import get_logger

logger = get_logger(__name__)


T = TypeVar("T")


class MessageSendResult(BaseModel, Generic[T]):
timestamp: datetime
message_context: Optional[T] = None


DestinationType = TypeVar("DestinationType")
MessageContextType = TypeVar("MessageContextType")


class BaseMessagingIntegration(ABC, Generic[DestinationType, MessageContextType]):
@abstractmethod
def send_message(
self,
destination: DestinationType,
body: MessageBody,
) -> MessageSendResult[MessageContextType]:
raise NotImplementedError

@abstractmethod
def supports_reply(self) -> bool:
raise NotImplementedError

def reply_to_message(
self,
destination: DestinationType,
message_context: MessageContextType,
body: MessageBody,
) -> MessageSendResult[MessageContextType]:
if not self.supports_reply():
raise MessageIntegrationReplyNotSupportedError
raise NotImplementedError
6 changes: 6 additions & 0 deletions elementary/messages/messaging_integrations/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class MessagingIntegrationError(Exception):
pass


class MessageIntegrationReplyNotSupportedError(MessagingIntegrationError):
pass
82 changes: 82 additions & 0 deletions elementary/messages/messaging_integrations/teams_webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from datetime import datetime
from typing import Optional

import requests
from pydantic import BaseModel

from elementary.messages.formats.adaptive_cards import format_adaptive_card
from elementary.messages.message_body import MessageBody
from elementary.messages.messaging_integrations.base_messaging_integration import (
BaseMessagingIntegration,
MessageSendResult,
)
from elementary.messages.messaging_integrations.exceptions import (
MessageIntegrationReplyNotSupportedError,
MessagingIntegrationError,
)
from elementary.utils.log import get_logger

logger = get_logger(__name__)


class ChannelWebhook(BaseModel):
webhook: str
channel: Optional[str] = None


def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of the comments in this method are redundant

"""Sends an Adaptive Card to the specified webhook URL."""
payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"contentUrl": None,
"content": card,
}
],
}

response = requests.post(
webhook_url,
json=payload,
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
if response.status_code == 202:
logger.debug("Got 202 response from Teams webhook, assuming success")
return response


class TeamsWebhookMessagingIntegration(
BaseMessagingIntegration[ChannelWebhook, ChannelWebhook]
):
def send_message(
self,
destination: ChannelWebhook,
body: MessageBody,
) -> MessageSendResult[ChannelWebhook]:
card = format_adaptive_card(body)
try:
send_adaptive_card(destination.webhook, card)
return MessageSendResult(
message_context=destination,
timestamp=datetime.utcnow(),
)
except requests.RequestException as e:
raise MessagingIntegrationError(
"Failed to send message to Teams webhook"
) from e

def supports_reply(self) -> bool:
return False

def reply_to_message(
self,
destination: ChannelWebhook,
message_context: ChannelWebhook,
body: MessageBody,
) -> MessageSendResult[ChannelWebhook]:
raise MessageIntegrationReplyNotSupportedError(
"Teams webhook message integration does not support replying to messages"
)
Loading
Loading