From 0c1ffdc89f2110859ca807f8e76241402e296ab8 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 7 Aug 2025 22:47:06 -0700 Subject: [PATCH] Add augmentable activity context --- packages/app/src/microsoft/teams/app/app.py | 17 +++++++++++++---- packages/app/src/microsoft/teams/app/options.py | 2 +- .../src/microsoft/teams/app/plugins/plugin.py | 8 +++++--- .../teams/app/plugins/plugin_activity_event.py | 5 ++--- .../teams/app/routing/activity_context.py | 12 +++++++++++- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/app/src/microsoft/teams/app/app.py b/packages/app/src/microsoft/teams/app/app.py index b4769351..835eadf8 100644 --- a/packages/app/src/microsoft/teams/app/app.py +++ b/packages/app/src/microsoft/teams/app/app.py @@ -8,10 +8,11 @@ import os from dataclasses import dataclass from logging import Logger -from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast, overload +from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, cast, overload from dotenv import find_dotenv, load_dotenv from microsoft.teams.api import ( + Activity, ActivityBase, ActivityTypeAdapter, ApiClient, @@ -22,6 +23,7 @@ JsonWebToken, TokenProtocol, ) +from microsoft.teams.app.plugins.plugin_activity_event import PluginActivityEvent from microsoft.teams.common import Client, ClientOptions, ConsoleLogger, EventEmitter, LocalStorage from .app_oauth import OauthHandlers @@ -43,6 +45,7 @@ version = importlib.metadata.version("microsoft-teams-app") F = TypeVar("F", bound=Callable[..., Any]) +R = TypeVar("R") load_dotenv(find_dotenv(usecwd=True)) USER_AGENT = f"teams.py[app]/{version}" @@ -87,7 +90,7 @@ def __init__(self, options: Optional[AppOptions] = None): self.http_client.clone(ClientOptions(token=lambda: self.tokens.bot)), ) - plugins: List[Plugin] = list(self.options.plugins or []) + plugins: List[Plugin[Any]] = list(self.options.plugins or []) http_plugin = None for i, plugin in enumerate(plugins): @@ -321,7 +324,7 @@ async def handle_activity(self, input_activity: HttpActivityEvent) -> Dict[str, try: self._events.emit("activity", ActivityEvent(activity)) - ctx = await self.build_context(activity, input_activity.token) + ctx = await self._build_context(activity, input_activity.token) response = await self._activity_processor.process_activity(ctx) await self.http.on_activity_response( PluginActivityResponseEvent( @@ -350,7 +353,7 @@ async def handle_activity(self, input_activity: HttpActivityEvent) -> Dict[str, ) raise - async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> ActivityContext[ActivityBase]: + async def _build_context(self, activity: Activity, token: TokenProtocol) -> ActivityContext[ActivityBase]: """Build the context object for activity processing. Args: @@ -389,6 +392,11 @@ async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> A # User token not available pass + context_by_plugin: dict[Type[Plugin[Any]], Any] = {} + for plugin in self.plugins: + ctx = await plugin.on_activity(PluginActivityEvent(sender=self.http, token=token, activity=activity)) + context_by_plugin[type(plugin)] = ctx + return ActivityContext( activity, self.id or "", @@ -399,6 +407,7 @@ async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> A conversation_ref, is_signed_in, self.options.default_connection_name, + context_by_plugin, ) @overload diff --git a/packages/app/src/microsoft/teams/app/options.py b/packages/app/src/microsoft/teams/app/options.py index a8879f86..c10d1ad0 100644 --- a/packages/app/src/microsoft/teams/app/options.py +++ b/packages/app/src/microsoft/teams/app/options.py @@ -24,7 +24,7 @@ class AppOptions: # Infrastructure logger: Optional[Logger] = None storage: Optional[Storage[str, Any]] = None - plugins: List[Plugin] = field(default_factory=list[Plugin]) + plugins: List[Plugin[Any]] = field(default_factory=list[Plugin[Any]]) enable_token_validation: bool = True # Oauth diff --git a/packages/app/src/microsoft/teams/app/plugins/plugin.py b/packages/app/src/microsoft/teams/app/plugins/plugin.py index 1d687c07..3de52f7c 100644 --- a/packages/app/src/microsoft/teams/app/plugins/plugin.py +++ b/packages/app/src/microsoft/teams/app/plugins/plugin.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from typing import Callable +from typing import Any, Callable, TypeVar from microsoft.teams.api.clients.conversation import ActivityParams from microsoft.teams.api.models import Resource @@ -27,8 +27,10 @@ Emitted when the plugin receives an activity """ +TExtraContext = TypeVar("TExtraContext", bound=dict[str, Any] | None) -class Plugin: + +class Plugin[TExtraContext]: """The base plugin for Teams app plugins.""" async def on_init(self) -> None: @@ -47,7 +49,7 @@ async def on_error(self, event: PluginErrorEvent) -> None: """Called by the App when an error occurs.""" ... - async def on_activity(self, event: PluginActivityEvent) -> None: + async def on_activity(self, event: PluginActivityEvent) -> TExtraContext: """Called by the App when an activity is received.""" ... diff --git a/packages/app/src/microsoft/teams/app/plugins/plugin_activity_event.py b/packages/app/src/microsoft/teams/app/plugins/plugin_activity_event.py index 3f92817b..9c9a21f0 100644 --- a/packages/app/src/microsoft/teams/app/plugins/plugin_activity_event.py +++ b/packages/app/src/microsoft/teams/app/plugins/plugin_activity_event.py @@ -3,16 +3,15 @@ Licensed under the MIT License. """ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple from microsoft.teams.api import Activity, TokenProtocol -from microsoft.teams.api.models.conversation import ConversationReference if TYPE_CHECKING: from .sender import Sender -class PluginActivityEvent(ConversationReference): +class PluginActivityEvent(NamedTuple): """Event emitted by a plugin when an activity is received.""" sender: "Sender" diff --git a/packages/app/src/microsoft/teams/app/routing/activity_context.py b/packages/app/src/microsoft/teams/app/routing/activity_context.py index c8e4848c..f8732cdc 100644 --- a/packages/app/src/microsoft/teams/app/routing/activity_context.py +++ b/packages/app/src/microsoft/teams/app/routing/activity_context.py @@ -7,7 +7,7 @@ import json from dataclasses import dataclass from logging import Logger -from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar +from typing import Any, Awaitable, Callable, Generic, Optional, Type, TypeVar from microsoft.teams.api import ( ActivityBase, @@ -32,6 +32,8 @@ from microsoft.teams.cards import AdaptiveCard from microsoft.teams.common import Storage +from ..plugins import Plugin + T = TypeVar("T", bound=ActivityBase, contravariant=True) SendCallable = Callable[[str | ActivityParams | AdaptiveCard], Awaitable[Resource]] @@ -65,6 +67,7 @@ def __init__( conversation_ref: ConversationReference, is_signed_in: bool, connection_name: str, + context_by_plugin: dict[Type[Plugin[Any]], Any], ): self.activity = activity self.app_id = app_id @@ -75,6 +78,7 @@ def __init__( self.user_token = user_token self.connection_name = connection_name self.is_signed_in = is_signed_in + self._context_by_plugin = context_by_plugin self._next_handler: Optional[Callable[[], Awaitable[None]]] = None @@ -235,3 +239,9 @@ async def sign_out(self) -> None: self.logger.debug(f"User {self.activity.from_.id} signed out successfully.") except Exception as e: self.logger.error(f"Failed to sign out user: {e}") + + def get_plugin_context[R](self, cls: Type[Plugin[R]]) -> R: + context = self._context_by_plugin.get(cls) + if context is None: + raise ValueError(f"No context found for plugin class {cls}") + return context