dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
340 lines
11 KiB
Python
340 lines
11 KiB
Python
import datetime
|
|
import json
|
|
import logging
|
|
import re
|
|
from http import HTTPStatus
|
|
from typing import Text, Dict, Any, List, Iterable, Callable, Awaitable, Optional
|
|
|
|
import jwt
|
|
import requests
|
|
from jwt import InvalidKeyError, PyJWTError
|
|
from jwt.algorithms import RSAAlgorithm
|
|
from requests import HTTPError
|
|
from sanic import Blueprint, response
|
|
from sanic.request import Request
|
|
from sanic.response import HTTPResponse
|
|
|
|
from rasa.core.channels.channel import UserMessage, OutputChannel, InputChannel
|
|
|
|
MICROSOFT_OPEN_ID_URI = (
|
|
"https://login.botframework.com/v1/.well-known/openidconfiguration"
|
|
)
|
|
|
|
BEARER_REGEX = re.compile(r"Bearer\s+(.*)")
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MICROSOFT_OAUTH2_URL = "https://login.microsoftonline.com"
|
|
|
|
MICROSOFT_OAUTH2_PATH = "botframework.com/oauth2/v2.0/token"
|
|
|
|
|
|
class BotFramework(OutputChannel):
|
|
"""A Microsoft Bot Framework communication channel."""
|
|
|
|
token_expiration_date = datetime.datetime.now()
|
|
|
|
headers = None
|
|
|
|
@classmethod
|
|
def name(cls) -> Text:
|
|
return "botframework"
|
|
|
|
def __init__(
|
|
self,
|
|
app_id: Text,
|
|
app_password: Text,
|
|
conversation: Dict[Text, Any],
|
|
bot: Text,
|
|
service_url: Text,
|
|
) -> None:
|
|
|
|
service_url = (
|
|
f"{service_url}/" if not service_url.endswith("/") else service_url
|
|
)
|
|
|
|
self.app_id = app_id
|
|
self.app_password = app_password
|
|
self.conversation = conversation
|
|
self.global_uri = f"{service_url}v3/"
|
|
self.bot = bot
|
|
|
|
async def _get_headers(self) -> Optional[Dict[Text, Any]]:
|
|
if BotFramework.token_expiration_date < datetime.datetime.now():
|
|
uri = f"{MICROSOFT_OAUTH2_URL}/{MICROSOFT_OAUTH2_PATH}"
|
|
grant_type = "client_credentials"
|
|
scope = "https://api.botframework.com/.default"
|
|
payload = {
|
|
"client_id": self.app_id,
|
|
"client_secret": self.app_password,
|
|
"grant_type": grant_type,
|
|
"scope": scope,
|
|
}
|
|
|
|
token_response = requests.post(uri, data=payload)
|
|
|
|
if token_response.ok:
|
|
token_data = token_response.json()
|
|
access_token = token_data["access_token"]
|
|
token_expiration = token_data["expires_in"]
|
|
|
|
delta = datetime.timedelta(seconds=int(token_expiration))
|
|
BotFramework.token_expiration_date = datetime.datetime.now() + delta
|
|
|
|
BotFramework.headers = {
|
|
"content-type": "application/json",
|
|
"Authorization": "Bearer %s" % access_token,
|
|
}
|
|
return BotFramework.headers
|
|
else:
|
|
logger.error("Could not get BotFramework token")
|
|
return None
|
|
else:
|
|
return BotFramework.headers
|
|
|
|
def prepare_message(
|
|
self, recipient_id: Text, message_data: Dict[Text, Any]
|
|
) -> Dict[Text, Any]:
|
|
data = {
|
|
"type": "message",
|
|
"recipient": {"id": recipient_id},
|
|
"from": self.bot,
|
|
"channelData": {"notification": {"alert": "true"}},
|
|
"text": "",
|
|
}
|
|
data.update(message_data)
|
|
return data
|
|
|
|
async def send(self, message_data: Dict[Text, Any]) -> None:
|
|
post_message_uri = "{}conversations/{}/activities".format(
|
|
self.global_uri, self.conversation["id"]
|
|
)
|
|
headers = await self._get_headers()
|
|
send_response = requests.post(
|
|
post_message_uri, headers=headers, data=json.dumps(message_data)
|
|
)
|
|
|
|
if not send_response.ok:
|
|
logger.error(
|
|
"Error trying to send botframework messge. Response: %s",
|
|
send_response.text,
|
|
)
|
|
|
|
async def send_text_message(
|
|
self, recipient_id: Text, text: Text, **kwargs: Any
|
|
) -> None:
|
|
for message_part in text.strip().split("\n\n"):
|
|
text_message = {"text": message_part}
|
|
message = self.prepare_message(recipient_id, text_message)
|
|
await self.send(message)
|
|
|
|
async def send_image_url(
|
|
self, recipient_id: Text, image: Text, **kwargs: Any
|
|
) -> None:
|
|
hero_content = {
|
|
"contentType": "application/vnd.microsoft.card.hero",
|
|
"content": {"images": [{"url": image}]},
|
|
}
|
|
|
|
image_message = {"attachments": [hero_content]}
|
|
message = self.prepare_message(recipient_id, image_message)
|
|
await self.send(message)
|
|
|
|
async def send_text_with_buttons(
|
|
self,
|
|
recipient_id: Text,
|
|
text: Text,
|
|
buttons: List[Dict[Text, Any]],
|
|
**kwargs: Any,
|
|
) -> None:
|
|
hero_content = {
|
|
"contentType": "application/vnd.microsoft.card.hero",
|
|
"content": {"subtitle": text, "buttons": buttons},
|
|
}
|
|
|
|
buttons_message = {"attachments": [hero_content]}
|
|
message = self.prepare_message(recipient_id, buttons_message)
|
|
await self.send(message)
|
|
|
|
async def send_elements(
|
|
self, recipient_id: Text, elements: Iterable[Dict[Text, Any]], **kwargs: Any
|
|
) -> None:
|
|
for e in elements:
|
|
message = self.prepare_message(recipient_id, e)
|
|
await self.send(message)
|
|
|
|
async def send_custom_json(
|
|
self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
|
|
) -> None:
|
|
json_message.setdefault("type", "message")
|
|
json_message.setdefault("recipient", {}).setdefault("id", recipient_id)
|
|
json_message.setdefault("from", self.bot)
|
|
json_message.setdefault("channelData", {}).setdefault(
|
|
"notification", {}
|
|
).setdefault("alert", "true")
|
|
json_message.setdefault("text", "")
|
|
await self.send(json_message)
|
|
|
|
|
|
class BotFrameworkInput(InputChannel):
|
|
"""Bot Framework input channel implementation."""
|
|
|
|
@classmethod
|
|
def name(cls) -> Text:
|
|
return "botframework"
|
|
|
|
@classmethod
|
|
def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
|
|
if not credentials:
|
|
cls.raise_missing_credentials_exception()
|
|
|
|
return cls(credentials.get("app_id"), credentials.get("app_password"))
|
|
|
|
def __init__(self, app_id: Text, app_password: Text) -> None:
|
|
"""Create a Bot Framework input channel.
|
|
|
|
Args:
|
|
app_id: Bot Framework's API id
|
|
app_password: Bot Framework application secret
|
|
"""
|
|
self.app_id = app_id
|
|
self.app_password = app_password
|
|
|
|
self.jwt_keys: Dict[Text, Any] = {}
|
|
self.jwt_update_time = datetime.datetime.fromtimestamp(0)
|
|
|
|
self._update_cached_jwk_keys()
|
|
|
|
def _update_cached_jwk_keys(self) -> None:
|
|
logger.debug("Updating JWT keys for the Botframework.")
|
|
response = requests.get(MICROSOFT_OPEN_ID_URI)
|
|
response.raise_for_status()
|
|
conf = response.json()
|
|
|
|
jwks_uri = conf["jwks_uri"]
|
|
|
|
keys_request = requests.get(jwks_uri)
|
|
keys_request.raise_for_status()
|
|
keys_list = keys_request.json()
|
|
self.jwt_keys = {key["kid"]: key for key in keys_list["keys"]}
|
|
self.jwt_update_time = datetime.datetime.now()
|
|
|
|
def _validate_jwt_token(self, jwt_token: Text) -> None:
|
|
jwt_header = jwt.get_unverified_header(jwt_token)
|
|
key_id = jwt_header["kid"]
|
|
if key_id not in self.jwt_keys:
|
|
raise InvalidKeyError(f"JWT Key with ID {key_id} not found.")
|
|
|
|
key_json = self.jwt_keys[key_id]
|
|
public_key = RSAAlgorithm.from_jwk(key_json)
|
|
jwt.decode(
|
|
jwt_token,
|
|
key=public_key,
|
|
audience=self.app_id,
|
|
algorithms=jwt_header["alg"],
|
|
)
|
|
|
|
def _validate_auth(self, auth_header: Optional[Text]) -> Optional[HTTPResponse]:
|
|
if not auth_header:
|
|
return response.text(
|
|
"No authorization header provided.", status=HTTPStatus.UNAUTHORIZED
|
|
)
|
|
|
|
# Update the JWT keys daily
|
|
if datetime.datetime.now() - self.jwt_update_time > datetime.timedelta(days=1):
|
|
try:
|
|
self._update_cached_jwk_keys()
|
|
except HTTPError as error:
|
|
logger.warning(
|
|
f"Could not update JWT keys from {MICROSOFT_OPEN_ID_URI}."
|
|
)
|
|
logger.exception(error, exc_info=True)
|
|
|
|
auth_match = BEARER_REGEX.match(auth_header)
|
|
if not auth_match:
|
|
return response.text(
|
|
"No Bearer token provided in Authorization header.",
|
|
status=HTTPStatus.UNAUTHORIZED,
|
|
)
|
|
|
|
(jwt_token,) = auth_match.groups()
|
|
|
|
try:
|
|
self._validate_jwt_token(jwt_token)
|
|
except PyJWTError as error:
|
|
logger.error("Bot framework JWT token could not be verified.")
|
|
logger.exception(error, exc_info=True)
|
|
return response.text(
|
|
"Could not validate JWT token.", status=HTTPStatus.UNAUTHORIZED
|
|
)
|
|
|
|
return None
|
|
|
|
@staticmethod
|
|
def add_attachments_to_metadata(
|
|
postdata: Dict[Text, Any], metadata: Optional[Dict[Text, Any]]
|
|
) -> Optional[Dict[Text, Any]]:
|
|
"""Merge the values of `postdata['attachments']` with `metadata`."""
|
|
if postdata.get("attachments"):
|
|
attachments = {"attachments": postdata["attachments"]}
|
|
if metadata:
|
|
metadata.update(attachments)
|
|
else:
|
|
metadata = attachments
|
|
return metadata
|
|
|
|
def blueprint(
|
|
self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
|
|
) -> Blueprint:
|
|
"""Defines the Sanic blueprint for the bot framework integration."""
|
|
botframework_webhook = Blueprint("botframework_webhook", __name__)
|
|
|
|
@botframework_webhook.route("/", methods=["GET"])
|
|
async def health(request: Request) -> HTTPResponse:
|
|
return response.json({"status": "ok"})
|
|
|
|
@botframework_webhook.route("/webhook", methods=["POST"])
|
|
async def webhook(request: Request) -> HTTPResponse:
|
|
validation_response = self._validate_auth(
|
|
request.headers.get("Authorization")
|
|
)
|
|
if validation_response:
|
|
return validation_response
|
|
|
|
postdata = request.json
|
|
metadata = self.get_metadata(request)
|
|
|
|
metadata_with_attachments = self.add_attachments_to_metadata(
|
|
postdata, metadata
|
|
)
|
|
|
|
try:
|
|
if postdata["type"] == "message":
|
|
out_channel = BotFramework(
|
|
self.app_id,
|
|
self.app_password,
|
|
postdata["conversation"],
|
|
postdata["recipient"],
|
|
postdata["serviceUrl"],
|
|
)
|
|
|
|
user_msg = UserMessage(
|
|
text=postdata.get("text", ""),
|
|
output_channel=out_channel,
|
|
sender_id=postdata["from"]["id"],
|
|
input_channel=self.name(),
|
|
metadata=metadata_with_attachments,
|
|
)
|
|
|
|
await on_new_message(user_msg)
|
|
else:
|
|
logger.info("Not received message type")
|
|
except Exception as e:
|
|
logger.error(f"Exception when trying to handle message.{e}")
|
|
logger.debug(e, exc_info=True)
|
|
pass
|
|
|
|
return response.text("success")
|
|
|
|
return botframework_webhook
|